Skip to content

Commit 6956a62

Browse files
authored
Item metadata REST endpoints: don't allow adding or removing semantics (#5390)
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
1 parent 61e2f74 commit 6956a62

7 files changed

Lines changed: 298 additions & 43 deletions

File tree

bundles/org.openhab.core.io.console/src/main/java/org/openhab/core/io/console/internal/extension/MetadataConsoleCommandExtension.java

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,21 @@ private void addMetadata(Console console, String itemName, String namespace, Str
147147
MetadataKey key = new MetadataKey(namespace, itemName);
148148
Map<String, Object> configMap = getConfigMap(config);
149149
Metadata metadata = new Metadata(key, value, configMap);
150-
if (metadataRegistry.get(key) != null) {
151-
metadataRegistry.update(metadata);
152-
console.println("Updated: " + metadata);
153-
} else {
154-
metadataRegistry.add(metadata);
155-
console.println("Added: " + metadata);
150+
try {
151+
if (metadataRegistry.get(key) == null) {
152+
metadataRegistry.add(metadata);
153+
console.println("Added: " + metadata);
154+
} else {
155+
if (metadataRegistry.update(metadata) == null) {
156+
console.println("Cannot update metadata in unmanaged provider: " + metadata);
157+
} else {
158+
console.println("Updated: " + metadata);
159+
}
160+
}
161+
} catch (UnsupportedOperationException e) {
162+
console.println("Namespace reserved in unmanaged provider: " + metadata);
163+
} catch (IllegalStateException e) {
164+
console.println("No managed provider available for: " + metadata);
156165
}
157166
}
158167
}
@@ -190,11 +199,25 @@ private void removeMetadata(Console console, String itemName, @Nullable String n
190199
}
191200

192201
private void removeMetadata(Console console, MetadataKey key) {
193-
Metadata metadata = metadataRegistry.remove(key);
194-
if (metadata != null) {
195-
console.println("Removed: " + metadata);
196-
} else {
197-
console.println("Metadata element for " + key + " could not be found.");
202+
try {
203+
if (metadataRegistry.get(key) != null) {
204+
Metadata removedMetadata = metadataRegistry.remove(key);
205+
if (removedMetadata != null) {
206+
console.println("Removed: " + removedMetadata);
207+
} else {
208+
if (metadataRegistry.get(key) != null) {
209+
console.println("Unmanaged metadata element for " + key + ", could not be removed.");
210+
} else {
211+
console.println("Metadata element for " + key + " could not be found.");
212+
}
213+
}
214+
} else {
215+
console.println("Metadata element for " + key + " could not be found.");
216+
}
217+
} catch (UnsupportedOperationException e) {
218+
console.println("Unmanaged metadata element for " + key + " in reserved namespace, could not be removed.");
219+
} catch (IllegalStateException e) {
220+
console.println("No managed provider available for metadata with key: " + key);
198221
}
199222
}
200223

@@ -205,7 +228,11 @@ private void orphan(Console console, String action, Collection<Metadata> metadat
205228
if (!itemNames.contains(md.getUID().getItemName())) {
206229
console.println("Item missing: " + md.getUID());
207230
if ("purge".equals(action)) {
208-
metadataRegistry.remove(md.getUID());
231+
try {
232+
metadataRegistry.remove(md.getUID());
233+
} catch (UnsupportedOperationException | IllegalStateException e) {
234+
// ignore metadata that cannot be removed
235+
}
209236
}
210237
}
211238
});

bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@
110110
import org.slf4j.Logger;
111111
import org.slf4j.LoggerFactory;
112112

113-
import com.google.gson.Gson;
114113
import com.google.gson.JsonObject;
115114

116115
import io.swagger.v3.oas.annotations.Operation;
@@ -182,8 +181,6 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context
182181
}
183182

184183
private final Logger logger = LoggerFactory.getLogger(ItemResource.class);
185-
private final Gson gson = new Gson();
186-
187184
private final DTOMapper dtoMapper;
188185
private final EventPublisher eventPublisher;
189186
private final ItemBuilderFactory itemBuilderFactory;
@@ -500,7 +497,7 @@ private Response sendItemStateInternal(@Nullable String language, String itemNam
500497
final Locale locale = localeService.getLocale(language);
501498
final ZoneId zoneId = timeZoneProvider.getTimeZone();
502499

503-
source = buildSource(source, securityContext);
500+
String eventSource = buildSource(source, securityContext);
504501

505502
// get Item
506503
Item item = getItem(itemName);
@@ -512,7 +509,7 @@ private Response sendItemStateInternal(@Nullable String language, String itemNam
512509

513510
if (state != null) {
514511
// set State and report OK
515-
eventPublisher.post(ItemEventFactory.createStateEvent(itemName, state, source));
512+
eventPublisher.post(ItemEventFactory.createStateEvent(itemName, state, eventSource));
516513
return getItemResponse(null, Status.ACCEPTED, null, locale, zoneId, null);
517514
} else {
518515
// State could not be parsed
@@ -566,7 +563,7 @@ private Response sendItemCommandInternal(String itemName, String value, @Nullabl
566563
SecurityContext securityContext) {
567564
Item item = getItem(itemName);
568565
Command command = null;
569-
source = buildSource(source, securityContext);
566+
String eventSource = buildSource(source, securityContext);
570567
if (item != null) {
571568
if ("toggle".equalsIgnoreCase(value) && (item instanceof SwitchItem || item instanceof RollershutterItem)) {
572569
if (OnOffType.ON.equals(item.getStateAs(OnOffType.class))) {
@@ -585,7 +582,7 @@ private Response sendItemCommandInternal(String itemName, String value, @Nullabl
585582
command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), value);
586583
}
587584
if (command != null) {
588-
eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command, source));
585+
eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command, eventSource));
589586
ResponseBuilder resbuilder = Response.ok();
590587
resbuilder.type(MediaType.TEXT_PLAIN);
591588
return resbuilder.build();
@@ -750,7 +747,8 @@ public Response removeTag(@PathParam("itemName") @Parameter(description = "item
750747
@ApiResponse(responseCode = "200", description = "OK"), //
751748
@ApiResponse(responseCode = "201", description = "Created"), //
752749
@ApiResponse(responseCode = "404", description = "Item not found."), //
753-
@ApiResponse(responseCode = "405", description = "Metadata not editable.") })
750+
@ApiResponse(responseCode = "405", description = "Metadata not editable."),
751+
@ApiResponse(responseCode = "503", description = "Managed provider not available.") })
754752
public Response addMetadata(@PathParam("itemName") @Parameter(description = "item name") String itemName,
755753
@PathParam("namespace") @Parameter(description = "namespace") String namespace,
756754
@Parameter(description = "metadata", required = true) MetadataDTO metadata) {
@@ -767,45 +765,81 @@ public Response addMetadata(@PathParam("itemName") @Parameter(description = "ite
767765

768766
MetadataKey key = new MetadataKey(namespace, itemName);
769767
Metadata md = new Metadata(key, value, metadata.config);
770-
if (metadataRegistry.get(key) == null) {
771-
metadataRegistry.add(md);
772-
return Response.status(Status.CREATED).type(MediaType.TEXT_PLAIN).build();
773-
} else {
774-
metadataRegistry.update(md);
775-
return Response.ok(null, MediaType.TEXT_PLAIN).build();
768+
try {
769+
if (metadataRegistry.get(key) == null) {
770+
metadataRegistry.add(md);
771+
return Response.status(Status.CREATED).type(MediaType.TEXT_PLAIN).build();
772+
} else {
773+
if (metadataRegistry.update(md) == null) {
774+
// Exists, but not managed
775+
return Response.status(Status.METHOD_NOT_ALLOWED).build();
776+
}
777+
return Response.ok(null, MediaType.TEXT_PLAIN).build();
778+
}
779+
} catch (UnsupportedOperationException e) {
780+
// Trying to add to a reserved namespace that is in an unmanaged provider
781+
return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, e.getMessage());
782+
} catch (IllegalStateException e) {
783+
// There is no managed provider available
784+
return Response.status(Status.SERVICE_UNAVAILABLE).build();
785+
}
786+
}
787+
788+
@DELETE
789+
@RolesAllowed({ Role.ADMIN })
790+
@Path("/{itemName: [a-zA-Z_0-9]+}/metadata")
791+
@Operation(operationId = "removeAllMetadataFromItem", summary = "Removes all managed metadata from an item.", security = {
792+
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
793+
@ApiResponse(responseCode = "200", description = "OK"),
794+
@ApiResponse(responseCode = "404", description = "Item not found.") })
795+
public Response removeAllMetadata(@PathParam("itemName") @Parameter(description = "item name") String itemName) {
796+
Item item = getItem(itemName);
797+
798+
if (item == null) {
799+
return Response.status(Status.NOT_FOUND).build();
776800
}
801+
802+
metadataRegistry.removeItemMetadata(itemName);
803+
return Response.ok(null, MediaType.TEXT_PLAIN).build();
777804
}
778805

779806
@DELETE
780807
@RolesAllowed({ Role.ADMIN })
781808
@Path("/{itemName: [a-zA-Z_0-9]+}/metadata/{namespace}")
782-
@Operation(operationId = "removeMetadataFromItem", summary = "Removes metadata from an item.", security = {
809+
@Operation(operationId = "removeMetadataFromItem", summary = "Removes metadata in a specific namespace from an item.", security = {
783810
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
784811
@ApiResponse(responseCode = "200", description = "OK"),
785-
@ApiResponse(responseCode = "404", description = "Item not found."),
786-
@ApiResponse(responseCode = "405", description = "Meta data not editable.") })
812+
@ApiResponse(responseCode = "404", description = "Item or namespace not found."),
813+
@ApiResponse(responseCode = "405", description = "Metadata not editable."),
814+
@ApiResponse(responseCode = "503", description = "Managed provider not available.") })
787815
public Response removeMetadata(@PathParam("itemName") @Parameter(description = "item name") String itemName,
788-
@Nullable @PathParam("namespace") @Parameter(description = "namespace") String namespace) {
816+
@PathParam("namespace") @Parameter(description = "namespace") String namespace) {
789817
Item item = getItem(itemName);
790818

791819
if (item == null) {
792820
return Response.status(Status.NOT_FOUND).build();
793821
}
794822

795-
if (namespace == null) {
796-
metadataRegistry.removeItemMetadata(itemName);
797-
} else {
798-
MetadataKey key = new MetadataKey(namespace, itemName);
823+
MetadataKey key = new MetadataKey(namespace, itemName);
824+
try {
799825
if (metadataRegistry.get(key) != null) {
800-
if (metadataRegistry.remove(key) == null) {
801-
return Response.status(Status.CONFLICT).build();
826+
Metadata removedMetadata = metadataRegistry.remove(key);
827+
if (removedMetadata != null) {
828+
return Response.ok(null, MediaType.TEXT_PLAIN).build();
829+
}
830+
if (metadataRegistry.get(key) != null) {
831+
// Exists, but not managed, and not removed in the mean time
832+
return Response.status(Status.METHOD_NOT_ALLOWED).build();
802833
}
803-
} else {
804-
return Response.status(Status.NOT_FOUND).build();
805834
}
835+
return Response.status(Status.NOT_FOUND).build();
836+
} catch (UnsupportedOperationException e) {
837+
// Trying to remove from a reserved namespace that is in an unmanaged provider
838+
return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED, e.getMessage());
839+
} catch (IllegalStateException e) {
840+
// There is no managed provider available
841+
return Response.status(Status.SERVICE_UNAVAILABLE).build();
806842
}
807-
808-
return Response.ok(null, MediaType.TEXT_PLAIN).build();
809843
}
810844

811845
@POST
@@ -818,8 +852,13 @@ public Response purge() {
818852
Collection<String> itemNames = itemRegistry.stream().map(Item::getName)
819853
.collect(Collectors.toCollection(HashSet::new));
820854

821-
metadataRegistry.getAll().stream().filter(md -> !itemNames.contains(md.getUID().getItemName()))
822-
.forEach(md -> metadataRegistry.remove(md.getUID()));
855+
metadataRegistry.getAll().stream().filter(md -> !itemNames.contains(md.getUID().getItemName())).forEach(md -> {
856+
try {
857+
metadataRegistry.remove(md.getUID());
858+
} catch (UnsupportedOperationException | IllegalStateException e) {
859+
// ignore metadata that cannot be removed
860+
}
861+
});
823862
return Response.ok().build();
824863
}
825864

bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.List;
1919
import java.util.Map;
2020
import java.util.Map.Entry;
21+
import java.util.Set;
2122
import java.util.TreeMap;
2223

2324
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -108,6 +109,11 @@ public Collection<Metadata> getAll() {
108109
return semantics.values();
109110
}
110111

112+
@Override
113+
public Collection<String> getReservedNamespaces() {
114+
return Set.of(NAMESPACE);
115+
}
116+
111117
/**
112118
* Updates the semantic metadata for an item and notifies all listeners about changes
113119
*

bundles/org.openhab.core/src/main/java/org/openhab/core/internal/items/MetadataRegistryImpl.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,16 @@
1313
package org.openhab.core.internal.items;
1414

1515
import java.util.Collection;
16+
import java.util.Map;
17+
import java.util.Set;
18+
import java.util.concurrent.ConcurrentHashMap;
1619
import java.util.stream.Collectors;
20+
import java.util.stream.Stream;
1721

1822
import org.eclipse.jdt.annotation.NonNullByDefault;
23+
import org.eclipse.jdt.annotation.Nullable;
1924
import org.openhab.core.common.registry.AbstractRegistry;
25+
import org.openhab.core.common.registry.Provider;
2026
import org.openhab.core.events.EventPublisher;
2127
import org.openhab.core.items.ManagedMetadataProvider;
2228
import org.openhab.core.items.Metadata;
@@ -31,18 +37,24 @@
3137
import org.osgi.service.component.annotations.Reference;
3238
import org.osgi.service.component.annotations.ReferenceCardinality;
3339
import org.osgi.service.component.annotations.ReferencePolicy;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
3442

3543
/**
3644
* This is the main implementing class of the {@link MetadataRegistry} interface. It
3745
* keeps track of all declared metadata of all metadata providers.
3846
*
3947
* @author Kai Kreuzer - Initial contribution
48+
* @author Mark Herwege - semantics namespace not in managed provider
4049
*/
4150
@Component(immediate = true, service = MetadataRegistry.class)
4251
@NonNullByDefault
4352
public class MetadataRegistryImpl extends AbstractRegistry<Metadata, MetadataKey, MetadataProvider>
4453
implements MetadataRegistry {
4554

55+
private final Logger logger = LoggerFactory.getLogger(MetadataRegistryImpl.class);
56+
private final Map<String, Set<MetadataProvider>> reservedNamespaces = new ConcurrentHashMap<>();
57+
4658
@Activate
4759
public MetadataRegistryImpl(final @Reference ReadyService readyService) {
4860
super(MetadataProvider.class);
@@ -114,4 +126,71 @@ public void removeItemMetadata(String itemName) {
114126
getManagedProvider()
115127
.ifPresent(managedProvider -> ((ManagedMetadataProvider) managedProvider).removeItemMetadata(itemName));
116128
}
129+
130+
@Override
131+
public Metadata add(Metadata element) {
132+
String namespace = element.getUID().getNamespace();
133+
Set<MetadataProvider> providers = reservedNamespaces.get(namespace);
134+
MetadataProvider managedProvider = (MetadataProvider) getManagedProvider().orElse(null);
135+
if (providers == null || providers.isEmpty() || providers.stream().anyMatch(p -> p.equals(managedProvider))) {
136+
return super.add(element);
137+
}
138+
throw new UnsupportedOperationException("Cannot add metadata to '" + namespace + "' namespace");
139+
}
140+
141+
@Override
142+
public @Nullable Metadata update(Metadata element) {
143+
String namespace = element.getUID().getNamespace();
144+
Set<MetadataProvider> providers = reservedNamespaces.get(namespace);
145+
MetadataProvider managedProvider = (MetadataProvider) getManagedProvider().orElse(null);
146+
if (providers == null || providers.isEmpty() || providers.stream().anyMatch(p -> p.equals(managedProvider))) {
147+
return super.update(element);
148+
}
149+
throw new UnsupportedOperationException("Cannot update metadata in '" + namespace + "' namespace");
150+
}
151+
152+
@Override
153+
public @Nullable Metadata remove(MetadataKey key) {
154+
String namespace = key.getNamespace();
155+
Set<MetadataProvider> providers = reservedNamespaces.get(namespace);
156+
MetadataProvider managedProvider = (MetadataProvider) getManagedProvider().orElse(null);
157+
if (providers == null || providers.isEmpty() || providers.stream().anyMatch(p -> p.equals(managedProvider))) {
158+
return super.remove(key);
159+
}
160+
throw new UnsupportedOperationException("Cannot remove metadata from '" + namespace + "' namespace");
161+
}
162+
163+
@Override
164+
protected void addProvider(Provider<Metadata> provider) {
165+
if (provider instanceof MetadataProvider metadataProvider) {
166+
metadataProvider.getReservedNamespaces().stream().forEach(namespace -> {
167+
Set<MetadataProvider> currentProviders = reservedNamespaces.getOrDefault(namespace, Set.of());
168+
if (!currentProviders.isEmpty()) {
169+
logger.debug("Multiple metadata providers are reserving namespace '{}', there should only be one.",
170+
namespace);
171+
}
172+
Set<MetadataProvider> providers = Stream
173+
.concat(currentProviders.stream(), Set.of(metadataProvider).stream())
174+
.collect(Collectors.toSet());
175+
reservedNamespaces.put(namespace, providers);
176+
});
177+
}
178+
super.addProvider(provider);
179+
}
180+
181+
@Override
182+
protected void removeProvider(Provider<Metadata> provider) {
183+
if (provider instanceof MetadataProvider metadataProvider) {
184+
metadataProvider.getReservedNamespaces().stream().forEach(namespace -> {
185+
Set<MetadataProvider> providers = reservedNamespaces.getOrDefault(namespace, Set.of()).stream()
186+
.filter(p -> !provider.equals(p)).collect(Collectors.toSet());
187+
if (providers.isEmpty()) {
188+
reservedNamespaces.remove(namespace);
189+
} else {
190+
reservedNamespaces.put(namespace, providers);
191+
}
192+
});
193+
}
194+
super.removeProvider(provider);
195+
}
117196
}

0 commit comments

Comments
 (0)