Skip to content

Item metadata REST endpoints: don't allow adding or removing semantics - #5390

Merged
holgerfriedrich merged 19 commits into
openhab:mainfrom
mherwege:semantics_rest
Mar 21, 2026
Merged

Item metadata REST endpoints: don't allow adding or removing semantics#5390
holgerfriedrich merged 19 commits into
openhab:mainfrom
mherwege:semantics_rest

Conversation

@mherwege

@mherwege mherwege commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Closes #5377

The PUT and DELETE /item/{itemname}/metadata/{namespace} endpoints allow adding/updating and removing metadata.

If you use this endpoint to add something in the semantics namespace when the item did not already have semantics defined, it will be stored in the managed provider.
A user identified issues when trying to use an LLM to update the item semantics (#5363 (comment)). The LLM added semantics to the metadata managed provider. Semantics metadata is auto-generated from item tags in the SemanticsMetadataProvider, and this is not a managed provider, so the REST endpoint should not write in it. The only way to remove it was to edit the JSONDB.

To avoid such issues, the REST endpoint should not allow changing anything in the semantics namespace.

An easy fix would have been to just check for the namespace in the REST API. The disadvantage would be that it then assumes the SemanticsMetadataProvider is part of the installation. If someone would ever remove or replace the SemanticsMetadataProvider, the semantics namespace would still not be available.
I therefore opted for a more general fix. I have introduce the concept of reserved namespace(s) for a provider. If there is a reserved namespace and the provider is not a managed provider, it will refuse add, update or delete operations.

This also fixes the DELETE /item/{itemname}/metadata/{namespace}. The intention in the code is to allow a null namespace to be passed. JAX-RS does not allow null path parameters and this would never have worked. I created an extra endpoint which ommits the namespace path parameter to delete all managed metadata for an item.

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
@mherwege
mherwege requested a review from a team as a code owner February 24, 2026 17:24
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
@mherwege
mherwege marked this pull request as draft February 24, 2026 22:12
@mherwege
mherwege marked this pull request as draft February 24, 2026 22:12
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
@mherwege
mherwege marked this pull request as ready for review February 25, 2026 05:42

@florian-h05 florian-h05 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

(No core maintainer though, so I cannot merge this.)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a “reserved metadata namespace” mechanism to prevent REST writes to computed/non-managed metadata (notably the semantics namespace), avoiding persistence into the managed metadata store and the resulting inconsistencies.

Changes:

  • Introduce MetadataProvider#getReservedNamespaces() to declare reserved metadata namespaces.
  • Enforce reserved-namespace write restrictions in MetadataRegistryImpl (block add/update/remove when the reserving provider is not managed).
  • Update item metadata REST endpoints to return 405 Method Not Allowed when metadata is not editable, and adjust integration test expectations accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
itests/.../ItemResourceOSGiTest.java Updates expected HTTP status for removing metadata provided by an unmanaged provider.
bundles/org.openhab.core/.../MetadataProvider.java Adds API for providers to declare reserved namespaces.
bundles/org.openhab.core/.../MetadataRegistryImpl.java Implements reserved-namespace tracking and blocks write operations for reserved namespaces owned by non-managed providers.
bundles/org.openhab.core.semantics/.../SemanticsMetadataProvider.java Declares semantics as a reserved namespace.
bundles/org.openhab.core.io.rest.core/.../ItemResource.java Maps registry write failures to 405 Method Not Allowed for metadata endpoints (PUT/DELETE).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +768 to 782
try {
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 {
if (metadataRegistry.update(md) == null) {
return Response.status(Status.METHOD_NOT_ALLOWED).build();
}
return Response.ok(null, MediaType.TEXT_PLAIN).build();
}
} catch (IllegalStateException e) {
return Response.status(Status.METHOD_NOT_ALLOWED.getStatusCode(), e.getMessage()).build();
}

Copilot AI Feb 28, 2026

Copy link

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

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.

@mherwege
mherwege marked this pull request as draft March 3, 2026 11:43
mherwege added 3 commits March 3, 2026 12:43
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 797 to 807
@@ -795,13 +805,22 @@ public Response removeMetadata(@PathParam("itemName") @Parameter(description = "
if (namespace == null) {
metadataRegistry.removeItemMetadata(itemName);
} else {

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is bound to @Path(.../metadata/{namespace}), so @PathParam("namespace") will never be null for real HTTP requests and the if (namespace == null) branch is effectively dead code. Consider removing @Nullable and the null-branch (or adding a separate endpoint without {namespace} if you intend to support deleting all metadata).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug that was already there, confirmed.

mherwege added 2 commits March 3, 2026 17:11
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

metadataRegistry.add(md);
return Response.status(Status.CREATED).type(MediaType.TEXT_PLAIN).build();
} else {
if (metadataRegistry.update(md) == null) {

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metadataRegistry.get(key) and metadataRegistry.update(md) are not atomic. If the metadata existed at the time of get but is removed concurrently before update, update can return null and this code will return 405 (“not editable”), which is misleading (it’s actually gone). A safer approach is to re-check metadataRegistry.get(key) when update returns null and return 404 if it’s now absent, reserving 405 for the “exists but not managed/editable” case.

Suggested change
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();
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

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.

mherwege added 2 commits March 4, 2026 09:36
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +131 to +139
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);
}
throw new UnsupportedOperationException("Cannot add metadata to '" + namespace + "' namespace");
}

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MetadataRegistryImpl.add/update/remove now throw UnsupportedOperationException for reserved namespaces. There are existing callers that invoke metadataRegistry.remove(...)/update(...) without handling this (e.g., the REST /items/metadata/purge implementation and the metadata console commands), which will now fail with an uncaught runtime exception whenever they touch reserved namespaces like semantics. Consider avoiding exceptions here (e.g., return null for update/remove and block add via a separate check) or update all internal callers to catch UnsupportedOperationException and handle it gracefully.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was throwing IllegalStateException before, which was also not handled.
I handle it now, and also updated the console commands to handle this properly.

mherwege added 2 commits March 4, 2026 11:42
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mherwege
mherwege marked this pull request as ready for review March 4, 2026 11:35
@mherwege

mherwege commented Mar 4, 2026

Copy link
Copy Markdown
Contributor Author

copilot is fully happy now.

@holgerfriedrich holgerfriedrich left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@holgerfriedrich
holgerfriedrich merged commit 6956a62 into openhab:main Mar 21, 2026
8 of 9 checks passed
@holgerfriedrich holgerfriedrich added the enhancement An enhancement or new feature of the Core label Mar 21, 2026
@holgerfriedrich holgerfriedrich added this to the 5.2 milestone Mar 21, 2026
@mherwege
mherwege deleted the semantics_rest branch March 21, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement An enhancement or new feature of the Core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prevent writing to semantics Metadata namespace via REST

4 participants