Skip to content

Implement a core sitemap registry#5004

Merged
kaikreuzer merged 10 commits into
openhab:mainfrom
mherwege:sitemap_registry
Mar 26, 2026
Merged

Implement a core sitemap registry#5004
kaikreuzer merged 10 commits into
openhab:mainfrom
mherwege:sitemap_registry

Conversation

@mherwege
Copy link
Copy Markdown
Contributor

@mherwege mherwege commented Sep 3, 2025

With this PR, a central sitemap registry is created. This registry has providers that offer sitemaps provide sitemaps from different providers. The current ones are DSL sitemaps and UI created sitemaps. The code for these has been refactored to implement such providers.

The reasons for doing this are:

  • Currently the UI created sitemaps are created as model sitemaps in core, residing in the org.openhab.core.model.sitemap, so org.openhab.core.ui depends on this model package.
  • Without a registry which is decoupled from the model, that same model depency would have to be included in a YAML sitemap provider.
  • With the current structure, it is impossible to show the file based sitemaps in the UI, as they are not converted to a common format. It is one way. The same would be true for converting these to YAML. It would have to be based on the sitemap representation in the model classes.

With the current structure, whatever we do when creating a new sitemap provider forces pulling the org.openhab.core.model.sitemap package directly or indirectly.
It therefore seems more appropriate to decouple the internal sitemap representation from the DSL model and xtext code generation.

See for the background: #4945 (comment) and #4945 (comment)

This PR prepares for:

  • Create a YAML sitemap provider.
  • Supporting a YAML format for sitemaps with the possibility to convert between them. See Sitemap DSL and YAML serialization #4945. This linked PR should be refactored to build on this one.
  • Refactor UI code to show (non-editable) representations of sitemaps provided from sitemap files or YAML files. This will require extra endpoints independent of the current once that only include configurations for UI managed sitemaps.
  • Do transparent conversions between the formats.

This PR is accompanied by the webui PR (openhab/openhab-webui#3349) that adapts the dependencies for BasicUI and CometVisu. Other UI's should not be concerned as they all work purely through the REST API and SSE events.

This is of course a major refactoring. Therefore there is a high risk and probability of some regressions and needs to be tested extensively. Code tests run through and my tests have not revealed issues so far. But I do expect there may be some.

@lolodomo I believe this is a better basis to continue work on YAML and DSL conversion afterwards.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements a core sitemap registry that decouples sitemap handling from the model packages. It creates a central registry for sitemaps with providers for different sources (DSL sitemaps and UI-created sitemaps) and introduces a new org.openhab.core.sitemap module with a factory-based approach for sitemap creation.

  • Creates a central SitemapRegistry to manage sitemaps from different providers
  • Introduces a new org.openhab.core.sitemap module with interfaces and implementations
  • Refactors existing UI sitemap provider to use the new registry architecture

Reviewed Changes

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

File Description
Feature/Karaf configurations Updates feature definitions to include new sitemap module and remove model dependencies
Test configurations Adds sitemap module dependency to integration test bundles
UI module refactoring Updates UI components to use new sitemap interfaces instead of model classes
New sitemap module Introduces core sitemap interfaces, registry, factory, and implementations

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@mherwege
Copy link
Copy Markdown
Contributor Author

mherwege commented Sep 4, 2025

There is a new package org.openhab.core.sitemap that has this sitemap registry code. org.openhab.core.ui still contains code that is only used for sitemaps (ItemUIRegistry, ChartProvider, ...). Should these be moved to this new package as well? Only UIComponentSitemapProvider would be left as long as managed sitemaps are stored in the UI component store.

@lolodomo
Copy link
Copy Markdown
Contributor

@mherwege : this message just to inform you that I am not ignoring your PR at all, I will review it.

@lolodomo
Copy link
Copy Markdown
Contributor

I started by reviewing org.openhab.core.sitemap. The only question I have is if you did not forget that it is a possible to have a default rule condition to set a particular value when none of the other conditions are valid.
Regarding the default value of some attributes, I have not checked that the default is the same as before.

@mherwege
Copy link
Copy Markdown
Contributor Author

The only question I have is if you did not forget that it is a possible to have a default rule condition to set a particular value when none of the other conditions are valid.

The behaviour should not have changed. item and condition in a Condition are nullable, only value is not.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 91 out of 91 changed files in this pull request and generated 7 comments.


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

Comment on lines 414 to 420
@Override
public void addModelChangeListener(ModelRepositoryChangeListener listener) {
modelChangeListeners.add(listener);
public void addProviderChangeListener(ProviderChangeListener<Sitemap> listener) {
super.addProviderChangeListener(listener);
getAll().forEach(sitemap -> {
notifyListenersAboutAddedElement(sitemap);
});
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

Overriding addProviderChangeListener and then calling notifyListenersAboutAddedElement for every existing sitemap will notify all registered listeners (not just the newly added one), causing duplicate/incorrect ADDED events. Consider removing this override, or explicitly backfilling only the newly added listener without broadcasting to others.

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.

Indeed not needed.

Comment on lines +425 to +427
getAll().forEach(sitemap -> {
notifyListenersAboutRemovedElement(sitemap);
});
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

Similarly, removeProviderChangeListener currently iterates all cached sitemaps and calls notifyListenersAboutRemovedElement, which will broadcast REMOVED events to the remaining listeners whenever any listener is removed. This is incorrect provider semantics and can cause consumers (e.g., registries) to drop all sitemaps unexpectedly. Remove this override or ensure notifications are not broadcast on listener removal.

Suggested change
getAll().forEach(sitemap -> {
notifyListenersAboutRemovedElement(sitemap);
});

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.

Indeed should not be there.

Comment on lines +140 to 164
private void refreshSitemapModels() {
sitemapCache.clear();
Iterable<String> sitemapFilenames = modelRepo.getAllModelNamesOfType(SITEMAP_MODEL_NAME);
for (String filename : sitemapFilenames) {
ModelSitemap modelSitemap = (ModelSitemap) modelRepo.getModel(filename);
if (modelSitemap != null) {
String sitemapName = filename.substring(0, filename.length() - SITEMAP_FILEEXT.length());
if (!modelSitemap.getName().equals(sitemapName)) {
logger.warn(
"Filename `{}` does not match the name `{}` of the sitemap - please fix this as you might see unexpected behavior otherwise.",
filename, modelSitemap.getName());
}
Sitemap sitemap = parseModelSitemap(modelSitemap);
sitemapCache.put(sitemapName, sitemap);
}
}
}

private Sitemap parseModelSitemap(ModelSitemap modelSitemap) {
Sitemap sitemap = sitemapFactory.createSitemap(modelSitemap.getName());
sitemap.setLabel(modelSitemap.getLabel());
sitemap.setIcon(modelSitemap.getIcon());
List<Widget> widgets = sitemap.getWidgets();
modelSitemap.getChildren().forEach(child -> addWidget(widgets, child, sitemap));
return sitemap;
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

refreshSitemapModels caches sitemaps under a key derived from the filename, but parseModelSitemap creates the Sitemap using modelSitemap.getName() as its UID/name. The registry indexes by Sitemap.getUID()/name, so a mismatch between filename and sitemap name will make sitemapRegistry.get(<filename>) fail and also makes oldSitemap = sitemapRegistry.get(sitemapName) unreliable. Consider normalizing so the cached key and the created sitemap UID match (e.g., create the sitemap with sitemapName, or cache/index by modelSitemap.getName() and treat mismatch as an error/ignore).

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 will reject if filename and sitemap name do not match. It creates potential problems downstream anyway when modifying the sitemap. This is not new though.

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 will reject if filename and sitemap name do not match. It creates potential problems downstream anyway when modifying the sitemap. This is not new though.

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.

I never understood why we have this constraint. That means we can have only one sitemap per file.
Probably something to improve in a next PR.

Comment on lines +85 to +102
def void checkDuplicates(ModelWidget w) {
val seen = new HashSet<String>
val duplicates = new HashSet<String>
val node = NodeModelUtils.getNode(w)
val line = node.startLine

for (leaf : node.leafNodes) {
val text = leaf.text.trim
if (KEYWORDS.contains(text) && !seen.add(text)) {
duplicates.add(text)
}
}

duplicates.forEach[attr |
val cleanAttr = attr.replaceAll("=$", "")
warning(errorString(getWidgetType(w) + " widget, attribute '" + cleanAttr + "' must not appear more than once", line), null)
]
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

checkDuplicates computes duplicates but is neither annotated with @Check nor called from any @Check method, so it will never run. Either add @Check (and an appropriate parameter type) or invoke it from other checks so duplicate-attribute warnings are actually emitted.

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.

OK

Comment on lines 104 to +111
@Check
def void checkFramesInFrame(Frame frame) {
for (Widget w : frame.children) {
if (w instanceof Frame) {
warning("Frames must not contain other frames",
SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.FRAME__CHILDREN));
return;
def void checkWidgetHasItem(ModelWidget w) {
if (!(w instanceof ModelFrame || w instanceof ModelText || w instanceof ModelImage || w instanceof ModelVideo || w instanceof ModelWebview || w instanceof ModelButtongrid) && w.item === null) {
val node = NodeModelUtils.getNode(w)
val line = node.startLine
error(errorString(getWidgetType(w) + " widget doesn't have item defined", line),
SitemapPackage.Literals.MODEL_WIDGET.getEStructuralFeature(SitemapPackage.MODEL_WIDGET__ITEM))
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

checkWidgetHasItem currently enforces item for most widgets but does not exempt ModelButton. In the grammar, ModelButton can omit item=, and buttons are typically used inside a Buttongrid that already carries the item. As-is, valid Buttongrid/Button usages without per-button items will become validation errors. Consider exempting ModelButton here (and relying on Buttongrid-level validation), or otherwise clarifying/enforcing the intended item semantics for buttons.

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 not correct. If the Button Widget is used, it does require a item defined on the Button. Button Widgets are always part of a Buttongrid. When Button Widgets are used the Buttongrid does not need to define an item (it will be ignored). The item on the Buttongrid is only relevant if the buttons attribute on the Buttongrid is used.

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.

I have a doubt, I will check that.

Copy link
Copy Markdown
Contributor Author

@mherwege mherwege Mar 23, 2026

Choose a reason for hiding this comment

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

I have a doubt, I will check that.

In the current sitemap syntax, it is required:

'Button' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? &

You could argue it could default to the Buttongrid item, but I don't think that is what happens today.

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.

Ok you're right

Comment on lines +120 to +126
warning(errorString(className + "widget has icon '" + w.icon + "' and staticIcon '" + w.staticIcon + "' defined at the same time", line), null)
}
if (w.iconRules !== null && w.staticIcon !== null) {
val node = NodeModelUtils.getNode(w)
val line = node.startLine
val iconRules = NodeModelUtils.getTokenText(NodeModelUtils.getNode(w.iconRules))
warning(errorString(className + "widget has icon rules '" + iconRules + "' and staticIcon '" + w.staticIcon + "' defined at the same time", line), null)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The warning messages in checkWidgetIcon concatenate className + "widget", producing messages like Textwidget ... (missing a space). Adjust the string to include a space for readability.

Suggested change
warning(errorString(className + "widget has icon '" + w.icon + "' and staticIcon '" + w.staticIcon + "' defined at the same time", line), null)
}
if (w.iconRules !== null && w.staticIcon !== null) {
val node = NodeModelUtils.getNode(w)
val line = node.startLine
val iconRules = NodeModelUtils.getTokenText(NodeModelUtils.getNode(w.iconRules))
warning(errorString(className + "widget has icon rules '" + iconRules + "' and staticIcon '" + w.staticIcon + "' defined at the same time", line), null)
warning(errorString(className + " widget has icon '" + w.icon + "' and staticIcon '" + w.staticIcon + "' defined at the same time", line), null)
}
if (w.iconRules !== null && w.staticIcon !== null) {
val node = NodeModelUtils.getNode(w)
val line = node.startLine
val iconRules = NodeModelUtils.getTokenText(NodeModelUtils.getNode(w.iconRules))
warning(errorString(className + " widget has icon rules '" + iconRules + "' and staticIcon '" + w.staticIcon + "' defined at the same time", line), null)

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.

OK

@kaikreuzer
Copy link
Copy Markdown
Member

Huge thanks @mherwege for this extensive PR and many thanks @lolodomo for your detailed review since the start!

I had a glance over it and the code looks really good. I just ran copilot for a last time and I think it has some relevant findings - would be great if you could look at them. We should then be fine to merge after the next milestone build.

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 91 out of 91 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 +375 to +412
Sitemap sitemap = null;
String sitemapFileName = modelName.substring(0, modelName.length() - SITEMAP_FILEEXT.length());
Sitemap oldSitemap = sitemapRegistry.get(sitemapFileName);

if (type == EventType.REMOVED) {
sitemapCache.remove(sitemapFileName);
} else {
EObject modelSitemapObject = modelRepo.getModel(modelName);
// if the sitemap file is empty it will not be in the repo and thus there is no need to cache it here
if (modelSitemapObject instanceof ModelSitemap modelSitemap) {
String sitemapName = modelSitemap.getName();
if (!sitemapFileName.equals(sitemapName)) {
logger.warn("Filename '{}' does not match the name '{}' of the sitemap - ignoring sitemap.",
sitemapFileName, sitemapName);
sitemapCache.remove(sitemapFileName);
} else {
sitemap = parseModelSitemap(modelSitemap);
sitemapCache.put(sitemapName, sitemap);
}
}
}

switch (type) {
case EventType.ADDED:
if (sitemap != null) {
notifyListenersAboutAddedElement(sitemap);
}
break;
case EventType.REMOVED:
if (oldSitemap != null) {
notifyListenersAboutRemovedElement(oldSitemap);
}
break;
case EventType.MODIFIED:
if (sitemap != null && oldSitemap != null) {
notifyListenersAboutUpdatedElement(oldSitemap, sitemap);
}
break;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

modelChanged only emits UPDATED events when both oldSitemap and the newly parsed sitemap are non-null. If a previously valid sitemap becomes invalid/empty or its internal name no longer matches the filename, the provider removes it from sitemapCache but does not emit a REMOVED event (and the registry will keep serving the stale sitemap). Similarly, if a previously invalid sitemap becomes valid again, no ADDED event is sent on MODIFIED. Consider deriving oldSitemap from the provider cache, and in the MODIFIED case emit REMOVED when oldSitemap != null && sitemap == null, emit ADDED when oldSitemap == null && sitemap != null, otherwise UPDATED when both are non-null.

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 can indeed be improved further.

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 91 out of 91 changed files in this pull request and generated 9 comments.


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

Comment on lines +71 to +72
org.openhab.core.semantics;version='[5.2.0,5.2.1)', \
org.openhab.core.sitemap;version='[5.2.0,5.2.1)'
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

In this bndrun snippet the new entry for org.openhab.core.sitemap is indented with spaces instead of the tab-indentation used for the other -runbundles entries. Keeping the same indentation style improves readability and helps avoid subtle formatting/parsing issues in long continued properties.

Copilot uses AI. Check for mistakes.
Comment on lines +397 to +403
Sitemap oldSitemap = sitemapCache.put(sitemapName, sitemap);
if (oldSitemap != null) {
notifyListenersAboutUpdatedElement(oldSitemap, sitemap);
} else {
notifyListenersAboutAddedElement(sitemap);
}
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

ModelRepository#getModel returns null when the DSL file is empty or cannot be parsed. In the MODIFIED/ADDED paths, if getModel(modelName) returns null (or not a ModelSitemap), the old sitemap remains in sitemapCache and listeners are not notified, so the registry/REST can continue serving a stale sitemap. Consider removing any cached entry for that filename and emitting a REMOVED notification when the refreshed model cannot be obtained/parsed.

Suggested change
Sitemap oldSitemap = sitemapCache.put(sitemapName, sitemap);
if (oldSitemap != null) {
notifyListenersAboutUpdatedElement(oldSitemap, sitemap);
} else {
notifyListenersAboutAddedElement(sitemap);
}
}
if (sitemap != null) {
Sitemap oldSitemap = sitemapCache.put(sitemapName, sitemap);
if (oldSitemap != null) {
notifyListenersAboutUpdatedElement(oldSitemap, sitemap);
} else {
notifyListenersAboutAddedElement(sitemap);
}
} else {
// Parsing failed, remove any previously cached sitemap and notify listeners
Sitemap oldSitemap = sitemapCache.remove(sitemapFileName);
if (oldSitemap != null) {
notifyListenersAboutRemovedElement(oldSitemap);
}
}
}
} else {
// Model could not be obtained or is not a ModelSitemap, remove any cached sitemap
Sitemap oldSitemap = sitemapCache.remove(sitemapFileName);
if (oldSitemap != null) {
notifyListenersAboutRemovedElement(oldSitemap);
}

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.

Yes, indeed.

break;
case Video videoWidget:
ModelVideo modelVideo = (ModelVideo) modelWidget;
videoWidget.setUrl(modelVideo.getUrl());
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

ModelVideo#url is optional in the DSL grammar, but this code passes it directly into Video#setUrl(String). If the model URL is absent, this can assign null into a field that is treated as non-null by the core sitemap API and downstream code. Consider guarding against null (e.g., keep the default/empty value) or only calling setUrl when a URL is actually present.

Suggested change
videoWidget.setUrl(modelVideo.getUrl());
String videoUrl = modelVideo.getUrl();
if (videoUrl != null) {
videoWidget.setUrl(videoUrl);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@mherwege mherwege Mar 22, 2026

Choose a reason for hiding this comment

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

Video url is not optional. It is checked in the sitemap validation, although it is only creating a warning. setUrl does accept null values and treats them properly.

Comment on lines +182 to +191
case Chart chartWidget:
ModelChart modelChart = (ModelChart) modelWidget;
chartWidget.setService(modelChart.getService());
chartWidget.setRefresh(modelChart.getRefresh());
chartWidget.setPeriod(modelChart.getPeriod());
chartWidget.setLegend(modelChart.getLegend());
chartWidget.setForceAsItem(modelChart.getForceAsItem());
chartWidget.setYAxisDecimalPattern(modelChart.getYAxisDecimalPattern());
chartWidget.setInterpolation(modelChart.getInterpolation());
break;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

ModelChart#period is optional in the DSL grammar, but the core sitemap Chart API treats period as a non-null string. Passing a missing period through here can result in null/invalid chart widgets later. Consider applying a default period (if the DSL has a default) or skipping/flagging the widget when period is absent.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@mherwege mherwege Mar 22, 2026

Choose a reason for hiding this comment

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

Webview period is not optional. It is checked in the sitemap validation. See above.

case Webview webviewWidget:
ModelWebview modelWebview = (ModelWebview) modelWidget;
webviewWidget.setHeight(modelWebview.getHeight());
webviewWidget.setUrl(modelWebview.getUrl());
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

ModelWebview#url is optional in the DSL grammar, but this code passes it into Webview#setUrl(String). If missing, this can propagate a null/invalid URL into the core sitemap representation and later REST/UI code. Consider guarding against null (e.g., keep default/empty) or rejecting/skipping invalid widgets.

Suggested change
webviewWidget.setUrl(modelWebview.getUrl());
String url = modelWebview.getUrl();
if (url != null && !url.isEmpty()) {
webviewWidget.setUrl(url);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@mherwege mherwege Mar 23, 2026

Choose a reason for hiding this comment

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

Webview url is not optional. It is checked in the sitemap validation. See above.

EList<ModelIconRule> modelIconRules = modelIconRuleList.getElements();
modelIconRules.forEach(modelIconRule -> {
Rule iconRule = sitemapFactory.createRule();
iconRule.setArgument(modelIconRule.getArg());
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

addWidgetIconRules sets the rule argument twice (setArgument(modelIconRule.getArg()) before and after copying conditions). This is redundant and makes the method harder to read/maintain; keep a single assignment.

Suggested change
iconRule.setArgument(modelIconRule.getArg());

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.

Indeed.

public void addModelChangeListener(ModelRepositoryChangeListener listener) {
modelChangeListeners.add(listener);
public Set<String> getSitemapNames() {
return sitemapCache.keySet();
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

getSitemapNames() returns sitemapCache.keySet(), which is a live view backed by the underlying ConcurrentHashMap and supports removal. Exposing this allows external callers to mutate the provider cache without going through provider notifications. Consider returning an immutable copy (e.g., Set.copyOf(...)).

Suggested change
return sitemapCache.keySet();
return Set.copyOf(sitemapCache.keySet());

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.

Sure, but not sure it is worth the copy overhead.

public void removeModelChangeListener(ModelRepositoryChangeListener listener) {
modelChangeListeners.remove(listener);
public Collection<Sitemap> getAll() {
return sitemapCache.values();
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

getAll() returns sitemapCache.values(), which is a live, mutable view of the underlying map. Since registries and other consumers may assume Provider#getAll() returns a stable snapshot, consider returning an immutable copy to prevent accidental external mutation and avoid concurrent-modification surprises.

Suggested change
return sitemapCache.values();
return List.copyOf(sitemapCache.values());

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.

Sure, but not sure it is worth the copy overhead.

Comment on lines +157 to +160
@Override
public String[] getSupportedWidgetTypes() {
return WIDGET_TYPES;
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

getSupportedWidgetTypes() returns the internal WIDGET_TYPES array directly. Since arrays are mutable, callers can accidentally modify the factory’s internal list of supported types. Consider returning a defensive copy (e.g., WIDGET_TYPES.clone()).

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.

Sure, but not sure it is worth the copy overhead.

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 91 out of 91 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
Copy link
Copy Markdown
Contributor Author

mherwege commented Mar 23, 2026

@kaikreuzer @lolodomo There were indeed some relevant copilot remarks. I made adjustmets accordingly (apart from the last 3 remarks, tell me if I should adjust that, I don't think this is different from before).
It looks like copilot is happy now.

Copy link
Copy Markdown
Member

@kaikreuzer kaikreuzer left a comment

Choose a reason for hiding this comment

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

Thanks you so much, @mherwege!
Since we released milestone 2 today, we can now merge this PR, start testing and building on it!

@kaikreuzer kaikreuzer merged commit d66f773 into openhab:main Mar 26, 2026
8 of 9 checks passed
@kaikreuzer kaikreuzer added the enhancement An enhancement or new feature of the Core label Mar 26, 2026
@kaikreuzer kaikreuzer added this to the 5.2 milestone Mar 26, 2026
lolodomo pushed a commit to openhab/openhab-webui that referenced this pull request Mar 27, 2026
Depends on openhab/openhab-core#5004

This is required if the core PR would be merged, or BasicUI and
CometVisu will not work anymore.

I have tested with BasicUI. I don't use CometVisu but have done similar
changes as for BasicUI.

---------

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
@mherwege mherwege deleted the sitemap_registry branch March 27, 2026 19:44
@lolodomo
Copy link
Copy Markdown
Contributor

lolodomo commented Apr 1, 2026

@mherwege : I am just finishing to read the last Copilot suggestions just before the merge and I believe the suggestion that modelVideo.getUrl() and modelWebview.getUrl() should be checked against null was a good suggestion , because the validation check is just a warning, not an error, so videoWidget.setUrl(url); and webviewWidget.setUrl(url); will be called with null and this is not what is expected.
So either we have to replace a warning by an error, or we have to check that the returned value is not null.
Same for the chart period.

@mherwege
Copy link
Copy Markdown
Contributor Author

mherwege commented Apr 1, 2026

@mherwege : I am just finishing to read the last Copilot suggestions just before the merge and I believe the suggestion that modelVideo.getUrl() and modelWebview.getUrl() should be checked against null was a good suggestion , because the validation check is just a warning, not an error, so videoWidget.setUrl(url); and webviewWidget.setUrl(url); will be called with null and this is not what is expected. So either we have to replace a warning by an error, or we have to check that the returned value is not null. Same for the chart period.

@lolodomo You are right. I can easily make the validation check an error, but that will make the whole sitemap fail. It would have failed before as well. Shall I include that in the Button validation PR, extending it to validation improvements?
The alternative is to do as copilot suggests here. This will then load the sitemap, but with a non-functional link. There is a warning in the log, but that may go unnoticed.
What is your preference?

@lolodomo
Copy link
Copy Markdown
Contributor

lolodomo commented Apr 1, 2026

Before your change, the sitemap will not have loaded as the field was mandatory in the syntax.
So I would suggest to change to an error message. That will force the user to investigate and fix his error.
I would prefer a separate PR.
The PR should cover all fields that were previously mandatory and which are no more now.

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.

6 participants