Skip to content

Conversation

mherwege
Copy link
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

@Copilot 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.

Comment on lines 260 to 261
try {
uri = createURIFromString(uriString);
uri = createURIFromString(uriString != null ? uriString : "");
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

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

Passing an empty string to createURIFromString when uriString is null may cause MalformedURLException or URISyntaxException. The original null check and early return should be preserved or handled appropriately.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will change that. The problem was there before but would generate a null pointer exception, as the model classes are not annotated.


private static final Pattern CONDITION_PATTERN = Pattern
.compile("((?<item>[A-Za-z]\\w*)?\\s*(?<condition>==|!=|<=|>=|<|>))?\\s*(?<sign>\\+|-)?(?<state>.+)");
.compile("((?<item>[A-Za-z]\\w*)?\\s*(?<condition>==|!=|<=|>=|<|>))?\\s*(?<value>(\\+|-)?.+)");
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

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

The regex pattern change from separate 'sign' and 'state' groups to a single 'value' group may break existing condition parsing. The original pattern allowed for more structured parsing of conditions.

Suggested change
.compile("((?<item>[A-Za-z]\\w*)?\\s*(?<condition>==|!=|<=|>=|<|>))?\\s*(?<value>(\\+|-)?.+)");
.compile("((?<item>[A-Za-z]\\w*)?\\s*(?<condition>==|!=|<=|>=|<|>))?\\s*(?<sign>\\+|-)?(?<state>.+)");

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The separate sign read into a separate field was only there to cope with a DSL parser limitation. The full value is needed, so no need to replicate these separate fields in the core representation of a condition value.

Comment on lines 302 to 304
mapping.setCmd(cmd != null ? cmd : "");
mapping.setReleaseCmd(releaseCmd);
mapping.setLabel(label);
mapping.setLabel(label != null ? label : "");
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

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

Setting empty strings as default values for cmd and label may cause issues if the consuming code expects null values for missing/unset commands and labels.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be fine.

Comment on lines 302 to 304
mapping.setCmd(cmd != null ? cmd : "");
mapping.setReleaseCmd(releaseCmd);
mapping.setLabel(label);
mapping.setLabel(label != null ? label : "");
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

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

Setting empty strings as default values for cmd and label may cause issues if the consuming code expects null values for missing/unset commands and labels.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be fine.

if (sourceRules instanceof Collection<?> sourceRulesCollection) {
for (Object sourceRule : sourceRulesCollection) {
if (sourceRule instanceof String) {
String argument = !key.equals("visibility") ? getRuleArgument(sourceRule.toString()) : null;
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

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

[nitpick] The hardcoded string comparison with 'visibility' makes the code brittle. Consider using constants or an enum for rule types to improve maintainability.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I doubt it is worth externalizing just this string.

import org.osgi.service.component.annotations.Component;

/**
* The {@link SitemapFactoryImpl} implements the {@link SitemapRegistry}
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

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

The class comment incorrectly states that SitemapFactoryImpl implements SitemapRegistry, but it actually implements SitemapFactory.

Suggested change
* The {@link SitemapFactoryImpl} implements the {@link SitemapRegistry}
* The {@link SitemapFactoryImpl} implements the {@link SitemapFactory}

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed.


@Override
public String getWidgetType() {
return this.getClass().getInterfaces()[0].getSimpleName();
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

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

Accessing the first interface without checking if any interfaces exist can cause ArrayIndexOutOfBoundsException. This implementation assumes the widget always implements exactly one interface in a specific order.

Suggested change
return this.getClass().getInterfaces()[0].getSimpleName();
Class<?>[] interfaces = this.getClass().getInterfaces();
if (interfaces.length > 0) {
return interfaces[0].getSimpleName();
} else {
return this.getClass().getSimpleName();
}

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it does assume it always implements the widget's interface and it is the first interface. I don't think these checks add much.

*
* @param visibility
*/
void setVisibility(List<Rule> visibility);
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

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

Parameter name 'visiblity' should be 'visibility' to match the method name and consistent naming convention.

Suggested change
void setVisibility(List<Rule> visibility);
void setVisibility(List<Rule> visibility);

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Already changed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Parameter name could be visibilityRules

@mherwege
Copy link
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.

Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
@lolodomo
Copy link
Contributor

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

List<ButtonDefinition> getButtons();

/**
* Replace the button grid buttons a new list of buttons.
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo: "with" is missing

*
* @return label color rules
*/
List<Rule> getLabelColor();
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe rather getLabelColorRules and setLabelColorRules ?

Copy link
Contributor Author

@mherwege mherwege Sep 20, 2025

Choose a reason for hiding this comment

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

Yes, that would be possible. But it would also mean the change in consuming classes is more then just changing the import. As much as possible, I kept the same method names from what they where before in the model, so I only had to change imports and do some cleanup. But now may be the right time to use more appropriate names as this is a major change anyway. @lolodomo What do you think? I left it as is for now.

*
* @return value color rules
*/
List<Rule> getValueColor();
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe rather getValueColorRules and setValueColorRules ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See above

*
* @return icon color rules
*/
List<Rule> getIconColor();
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe rather getIconColorRules and setIconColorRules ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See above

*
* @return visibility rules
*/
List<Rule> getVisibility();
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe rather getVisibilityRules and setVisibilityRules ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See above

* Returns the sitemap name.
*
* @return sitemap name.
* @see #setName(String)
Copy link
Contributor

@lolodomo lolodomo Sep 20, 2025

Choose a reason for hiding this comment

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

All these @see in this interface looks like useless as they just reference them each other.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ended there because they started as copies of the generated model classes, but will remove.

Comment on lines 39 to 42
private List<Rule> labelColor = new CopyOnWriteArrayList<>();
private List<Rule> valueColor = new CopyOnWriteArrayList<>();
private List<Rule> iconColor = new CopyOnWriteArrayList<>();
private List<Rule> visibility = new CopyOnWriteArrayList<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest labelColorRules, valueColorRules, ... etc


@Override
protected void notifyListenersAboutUpdatedElement(Sitemap oldElement, Sitemap element) {
registryChangeListeners.forEach(listener -> listener.updated(oldElement, element));
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no notify method to call in super class ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This could probably be much more simple. As I ended up extending AbstractRegistry, only the last 2 methods (addSitemapProver and removeSitemapProvider) are needed. AbstractRegistry keeps track of the listeners itself. I will make the change that way, but will need to do a full test.

@lolodomo
Copy link
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.

case Default defaultWidget:
setWidgetPropertyFromComponentConfig(defaultWidget, component, "height");
break;
default:
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like you forgot the case of Switch to call addWidgetMappings

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, thank you.

Comment on lines 302 to 304
mapping.setCmd(cmd != null ? cmd : "");
mapping.setReleaseCmd(releaseCmd);
mapping.setLabel(label);
mapping.setLabel(label != null ? label : "");
Copy link
Contributor

Choose a reason for hiding this comment

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

For consistency, you could do the same as in addWidgetButtons, that is testing if value is not null before calling setCmd and setLabel.

@Test
public void getWidgetUnknownPageId() throws ItemNotFoundException {
Sitemap sitemap = SitemapFactory.eINSTANCE.createSitemap();
Sitemap sitemap = sitemapFactoryMock.createSitemap(SITEMAP_NAME);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it normal to not have any when(sitemapFactoryMock.createSitemap) ?

Copy link
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 understand your question here. I can't create a sitemap for testing anymore the same way as before when it was using the model. So I reverted to a mock.

if (itemToBeSent != null) {
String widgetTypeName = widget.eClass().getInstanceTypeName()
.substring(widget.eClass().getInstanceTypeName().lastIndexOf(".") + 1);
String widgetTypeName = widget.getClass().getSimpleName();
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather widget.getWidgetType() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Missed this when creating the method in Widget.

@mherwege
Copy link
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.

Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants