-
-
Notifications
You must be signed in to change notification settings - Fork 470
Item metadata REST endpoints: don't allow adding or removing semantics #5390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
da78b82
abab090
2648c75
8b392f7
fcb7522
0178239
355e1d3
6f1ea7c
97da6e1
1301f56
5e3a4a0
5b25110
6c4d37d
9ba5b7f
f962ed2
10267ae
de19943
39e195a
81b14fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -110,7 +110,6 @@ | |||||||||||||||
| import org.slf4j.Logger; | ||||||||||||||||
| import org.slf4j.LoggerFactory; | ||||||||||||||||
|
|
||||||||||||||||
| import com.google.gson.Gson; | ||||||||||||||||
| import com.google.gson.JsonObject; | ||||||||||||||||
|
|
||||||||||||||||
| import io.swagger.v3.oas.annotations.Operation; | ||||||||||||||||
|
|
@@ -182,8 +181,6 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private final Logger logger = LoggerFactory.getLogger(ItemResource.class); | ||||||||||||||||
| private final Gson gson = new Gson(); | ||||||||||||||||
|
|
||||||||||||||||
| private final DTOMapper dtoMapper; | ||||||||||||||||
| private final EventPublisher eventPublisher; | ||||||||||||||||
| private final ItemBuilderFactory itemBuilderFactory; | ||||||||||||||||
|
|
@@ -500,7 +497,7 @@ private Response sendItemStateInternal(@Nullable String language, String itemNam | |||||||||||||||
| final Locale locale = localeService.getLocale(language); | ||||||||||||||||
| final ZoneId zoneId = timeZoneProvider.getTimeZone(); | ||||||||||||||||
|
|
||||||||||||||||
| source = buildSource(source, securityContext); | ||||||||||||||||
| String eventSource = buildSource(source, securityContext); | ||||||||||||||||
|
|
||||||||||||||||
| // get Item | ||||||||||||||||
| Item item = getItem(itemName); | ||||||||||||||||
|
|
@@ -512,7 +509,7 @@ private Response sendItemStateInternal(@Nullable String language, String itemNam | |||||||||||||||
|
|
||||||||||||||||
| if (state != null) { | ||||||||||||||||
| // set State and report OK | ||||||||||||||||
| eventPublisher.post(ItemEventFactory.createStateEvent(itemName, state, source)); | ||||||||||||||||
| eventPublisher.post(ItemEventFactory.createStateEvent(itemName, state, eventSource)); | ||||||||||||||||
| return getItemResponse(null, Status.ACCEPTED, null, locale, zoneId, null); | ||||||||||||||||
| } else { | ||||||||||||||||
| // State could not be parsed | ||||||||||||||||
|
|
@@ -566,7 +563,7 @@ private Response sendItemCommandInternal(String itemName, String value, @Nullabl | |||||||||||||||
| SecurityContext securityContext) { | ||||||||||||||||
| Item item = getItem(itemName); | ||||||||||||||||
| Command command = null; | ||||||||||||||||
| source = buildSource(source, securityContext); | ||||||||||||||||
| String eventSource = buildSource(source, securityContext); | ||||||||||||||||
| if (item != null) { | ||||||||||||||||
| if ("toggle".equalsIgnoreCase(value) && (item instanceof SwitchItem || item instanceof RollershutterItem)) { | ||||||||||||||||
| if (OnOffType.ON.equals(item.getStateAs(OnOffType.class))) { | ||||||||||||||||
|
|
@@ -585,7 +582,7 @@ private Response sendItemCommandInternal(String itemName, String value, @Nullabl | |||||||||||||||
| command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), value); | ||||||||||||||||
| } | ||||||||||||||||
| if (command != null) { | ||||||||||||||||
| eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command, source)); | ||||||||||||||||
| eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command, eventSource)); | ||||||||||||||||
| ResponseBuilder resbuilder = Response.ok(); | ||||||||||||||||
| resbuilder.type(MediaType.TEXT_PLAIN); | ||||||||||||||||
| return resbuilder.build(); | ||||||||||||||||
|
|
@@ -750,7 +747,8 @@ public Response removeTag(@PathParam("itemName") @Parameter(description = "item | |||||||||||||||
| @ApiResponse(responseCode = "200", description = "OK"), // | ||||||||||||||||
| @ApiResponse(responseCode = "201", description = "Created"), // | ||||||||||||||||
| @ApiResponse(responseCode = "404", description = "Item not found."), // | ||||||||||||||||
| @ApiResponse(responseCode = "405", description = "Metadata not editable.") }) | ||||||||||||||||
| @ApiResponse(responseCode = "405", description = "Metadata not editable."), | ||||||||||||||||
| @ApiResponse(responseCode = "503", description = "Managed provider not available.") }) | ||||||||||||||||
| public Response addMetadata(@PathParam("itemName") @Parameter(description = "item name") String itemName, | ||||||||||||||||
| @PathParam("namespace") @Parameter(description = "namespace") String namespace, | ||||||||||||||||
| @Parameter(description = "metadata", required = true) MetadataDTO metadata) { | ||||||||||||||||
|
|
@@ -767,45 +765,81 @@ public Response addMetadata(@PathParam("itemName") @Parameter(description = "ite | |||||||||||||||
|
|
||||||||||||||||
| MetadataKey key = new MetadataKey(namespace, itemName); | ||||||||||||||||
| Metadata md = new Metadata(key, value, metadata.config); | ||||||||||||||||
| if (metadataRegistry.get(key) == null) { | ||||||||||||||||
| metadataRegistry.add(md); | ||||||||||||||||
| return Response.status(Status.CREATED).type(MediaType.TEXT_PLAIN).build(); | ||||||||||||||||
| } else { | ||||||||||||||||
| metadataRegistry.update(md); | ||||||||||||||||
| return Response.ok(null, MediaType.TEXT_PLAIN).build(); | ||||||||||||||||
| try { | ||||||||||||||||
| if (metadataRegistry.get(key) == null) { | ||||||||||||||||
| metadataRegistry.add(md); | ||||||||||||||||
| return Response.status(Status.CREATED).type(MediaType.TEXT_PLAIN).build(); | ||||||||||||||||
| } else { | ||||||||||||||||
| if (metadataRegistry.update(md) == null) { | ||||||||||||||||
|
||||||||||||||||
| if (metadataRegistry.update(md) == null) { | |
| Metadata previous = metadataRegistry.update(md); | |
| if (previous == null) { | |
| // Metadata may have been removed concurrently; re-check existence | |
| if (metadataRegistry.get(key) == null) { | |
| return Response.status(Status.NOT_FOUND).build(); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is a good idea. You try to add. Responding you are not adding because the metadata does not exist is not what you would expect. You could try to add again, but you get in a potential loop. I prefer the current not allowed.
Copilot
AI
Feb 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching all IllegalStateException here will also convert unrelated registry failures (e.g., ManagedProvider is not available thrown by AbstractRegistry.add/update/remove) into a 405, which can mask real server/configuration problems. It would be safer to throw/catch a dedicated exception type for “reserved namespace not editable” (e.g., UnsupportedOperationException or a custom runtime exception) and let other IllegalStateExceptions propagate/return a 500-style error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am using UnsupportedOperationException now. But looking at the previous code, while the API documentation said it was returning 405 if the metadata was not editable, it did not do that. The IllegalStateException was also never catched and not returned by the REST API. Not having a managed provider should be an acceptable state. It just means nothing can be edited through the REST API or UI. But it should be handled in my opinion.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,10 +13,16 @@ | |
| package org.openhab.core.internal.items; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
|
|
||
| import org.eclipse.jdt.annotation.NonNullByDefault; | ||
| import org.eclipse.jdt.annotation.Nullable; | ||
| import org.openhab.core.common.registry.AbstractRegistry; | ||
| import org.openhab.core.common.registry.Provider; | ||
| import org.openhab.core.events.EventPublisher; | ||
| import org.openhab.core.items.ManagedMetadataProvider; | ||
| import org.openhab.core.items.Metadata; | ||
|
|
@@ -31,18 +37,24 @@ | |
| import org.osgi.service.component.annotations.Reference; | ||
| import org.osgi.service.component.annotations.ReferenceCardinality; | ||
| import org.osgi.service.component.annotations.ReferencePolicy; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * This is the main implementing class of the {@link MetadataRegistry} interface. It | ||
| * keeps track of all declared metadata of all metadata providers. | ||
| * | ||
| * @author Kai Kreuzer - Initial contribution | ||
| * @author Mark Herwege - semantics namespace not in managed provider | ||
| */ | ||
| @Component(immediate = true, service = MetadataRegistry.class) | ||
| @NonNullByDefault | ||
| public class MetadataRegistryImpl extends AbstractRegistry<Metadata, MetadataKey, MetadataProvider> | ||
| implements MetadataRegistry { | ||
|
|
||
| private final Logger logger = LoggerFactory.getLogger(MetadataRegistryImpl.class); | ||
| private final Map<String, Set<MetadataProvider>> reservedNamespaces = new ConcurrentHashMap<>(); | ||
|
|
||
| @Activate | ||
| public MetadataRegistryImpl(final @Reference ReadyService readyService) { | ||
| super(MetadataProvider.class); | ||
|
|
@@ -114,4 +126,71 @@ public void removeItemMetadata(String itemName) { | |
| getManagedProvider() | ||
| .ifPresent(managedProvider -> ((ManagedMetadataProvider) managedProvider).removeItemMetadata(itemName)); | ||
| } | ||
|
|
||
| @Override | ||
| public Metadata add(Metadata element) { | ||
| String namespace = element.getUID().getNamespace(); | ||
| Set<MetadataProvider> providers = reservedNamespaces.get(namespace); | ||
| MetadataProvider managedProvider = (MetadataProvider) getManagedProvider().orElse(null); | ||
| if (providers == null || providers.isEmpty() || providers.stream().anyMatch(p -> p.equals(managedProvider))) { | ||
| return super.add(element); | ||
|
mherwege marked this conversation as resolved.
|
||
| } | ||
| throw new UnsupportedOperationException("Cannot add metadata to '" + namespace + "' namespace"); | ||
| } | ||
|
Comment on lines
+131
to
+139
|
||
|
|
||
| @Override | ||
| public @Nullable Metadata update(Metadata element) { | ||
| String namespace = element.getUID().getNamespace(); | ||
| Set<MetadataProvider> providers = reservedNamespaces.get(namespace); | ||
| MetadataProvider managedProvider = (MetadataProvider) getManagedProvider().orElse(null); | ||
| if (providers == null || providers.isEmpty() || providers.stream().anyMatch(p -> p.equals(managedProvider))) { | ||
| return super.update(element); | ||
| } | ||
| throw new UnsupportedOperationException("Cannot update metadata in '" + namespace + "' namespace"); | ||
| } | ||
|
|
||
| @Override | ||
| public @Nullable Metadata remove(MetadataKey key) { | ||
| String namespace = key.getNamespace(); | ||
| Set<MetadataProvider> providers = reservedNamespaces.get(namespace); | ||
| MetadataProvider managedProvider = (MetadataProvider) getManagedProvider().orElse(null); | ||
| if (providers == null || providers.isEmpty() || providers.stream().anyMatch(p -> p.equals(managedProvider))) { | ||
| return super.remove(key); | ||
| } | ||
| throw new UnsupportedOperationException("Cannot remove metadata from '" + namespace + "' namespace"); | ||
| } | ||
|
|
||
| @Override | ||
| protected void addProvider(Provider<Metadata> provider) { | ||
| if (provider instanceof MetadataProvider metadataProvider) { | ||
| metadataProvider.getReservedNamespaces().stream().forEach(namespace -> { | ||
| Set<MetadataProvider> currentProviders = reservedNamespaces.getOrDefault(namespace, Set.of()); | ||
| if (!currentProviders.isEmpty()) { | ||
| logger.debug("Multiple metadata providers are reserving namespace '{}', there should only be one.", | ||
| namespace); | ||
| } | ||
| Set<MetadataProvider> providers = Stream | ||
| .concat(currentProviders.stream(), Set.of(metadataProvider).stream()) | ||
| .collect(Collectors.toSet()); | ||
| reservedNamespaces.put(namespace, providers); | ||
| }); | ||
| } | ||
|
mherwege marked this conversation as resolved.
|
||
| super.addProvider(provider); | ||
| } | ||
|
|
||
| @Override | ||
| protected void removeProvider(Provider<Metadata> provider) { | ||
| if (provider instanceof MetadataProvider metadataProvider) { | ||
| metadataProvider.getReservedNamespaces().stream().forEach(namespace -> { | ||
| Set<MetadataProvider> providers = reservedNamespaces.getOrDefault(namespace, Set.of()).stream() | ||
| .filter(p -> !provider.equals(p)).collect(Collectors.toSet()); | ||
| if (providers.isEmpty()) { | ||
| reservedNamespaces.remove(namespace); | ||
| } else { | ||
| reservedNamespaces.put(namespace, providers); | ||
| } | ||
| }); | ||
| } | ||
|
mherwege marked this conversation as resolved.
mherwege marked this conversation as resolved.
|
||
| super.removeProvider(provider); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.