Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion bundles/org.openhab.core.io.rest.core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,26 @@
<artifactId>org.openhab.core.semantics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.sitemap</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.compile-model</artifactId>
<type>pom</type>
</dependency>
Comment on lines +68 to +82
Copy link
Contributor

@lolodomo lolodomo Aug 24, 2025

Choose a reason for hiding this comment

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

With the move of the 2 classes SitemapFileGenerator and AbstractSitemapFileGenerator in org.openhab.core.ui, just adding a dependency to org.openhab.core.ui should be sufficient.

<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -57,6 +59,9 @@
import org.openhab.core.items.MetadataKey;
import org.openhab.core.items.MetadataRegistry;
import org.openhab.core.items.fileconverter.ItemFileGenerator;
import org.openhab.core.model.sitemap.SitemapProvider;
import org.openhab.core.model.sitemap.fileconverter.SitemapFileGenerator;
import org.openhab.core.model.sitemap.sitemap.Sitemap;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
Expand Down Expand Up @@ -92,12 +97,13 @@

/**
* This class acts as a REST resource and provides different methods to generate file format
* for existing items and things.
* for existing items, things and sitemaps.
*
* This resource is registered with the Jersey servlet.
*
* @author Laurent Garnier - Initial contribution
* @author Laurent Garnier - Add YAML output for things
* @author Mark Herwege - Add sitemap DSL
*/
@Component
@JaxrsResource
Expand Down Expand Up @@ -175,6 +181,26 @@ public class FileFormatResource implements RESTResource {
param: my param value
""";

private static final String DSL_SITEMAPS_EXAMPLE = """
sitemap MySitemap label="My Sitemap" {
Frame {
Input item=MyItem label="My Input"
}
}
""";

private static final String YAML_SITEMAPS_EXAMPLE = """
Comment on lines +184 to +192
Copy link
Contributor

Choose a reason for hiding this comment

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

Please provide the same example in both formats.

version: 2
Copy link
Contributor

@lolodomo lolodomo Aug 24, 2025

Choose a reason for hiding this comment

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

version should be 1.

I see it was kept to 2 for other examples in that file, that is to be fixed in a separate PR so that it can be easily backported to 5.0.x. I will create it.

sitemaps:
MySitemap:
label: Label
widgets:
MyWidget:
type: Switch
label: Label
item: MyItem
""";

private final Logger logger = LoggerFactory.getLogger(FileFormatResource.class);

private final ItemRegistry itemRegistry;
Expand All @@ -184,8 +210,10 @@ public class FileFormatResource implements RESTResource {
private final Inbox inbox;
private final ThingTypeRegistry thingTypeRegistry;
private final ConfigDescriptionRegistry configDescRegistry;
private final List<SitemapProvider> sitemapProviders = new ArrayList<>();
private final Map<String, ItemFileGenerator> itemFileGenerators = new ConcurrentHashMap<>();
private final Map<String, ThingFileGenerator> thingFileGenerators = new ConcurrentHashMap<>();
private final Map<String, SitemapFileGenerator> sitemapFileGenerators = new ConcurrentHashMap<>();

@Activate
public FileFormatResource(//
Expand Down Expand Up @@ -227,6 +255,24 @@ protected void removeThingFileGenerator(ThingFileGenerator thingFileGenerator) {
thingFileGenerators.remove(thingFileGenerator.getFileFormatGenerator());
}

@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
protected void addSitemapFileGenerator(SitemapFileGenerator sitemapFileGenerator) {
sitemapFileGenerators.put(sitemapFileGenerator.getFileFormatGenerator(), sitemapFileGenerator);
}

protected void removeSitemapFileGenerator(SitemapFileGenerator sitemapFileGenerator) {
sitemapFileGenerators.remove(sitemapFileGenerator.getFileFormatGenerator());
}

@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
protected void addSitemapProvider(SitemapProvider sitemapProvider) {
sitemapProviders.add(sitemapProvider);
}

protected void removeSitemapProvider(SitemapProvider sitemapProvider) {
sitemapProviders.remove(sitemapProvider);
}

@POST
@RolesAllowed({ Role.ADMIN })
@Path("/items")
Expand Down Expand Up @@ -305,6 +351,55 @@ public Response createFileFormatForThings(final @Context HttpHeaders httpHeaders
return Response.ok(new String(outputStream.toByteArray())).build();
}

@POST
@RolesAllowed({ Role.ADMIN })
@Path("/sitemaps")
@Consumes(MediaType.APPLICATION_JSON)
@Produces({ "text/vnd.openhab.dsl.sitemap", "application/yaml" })
@Operation(operationId = "createFileFormatForSitemaps", summary = "Create file format for a list of sitemaps in registry.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = {
@Content(mediaType = "text/vnd.openhab.dsl.sitemap", schema = @Schema(example = DSL_SITEMAPS_EXAMPLE)),
@Content(mediaType = "application/yaml", schema = @Schema(example = YAML_SITEMAPS_EXAMPLE)) }),
@ApiResponse(responseCode = "404", description = "One or more sitemaps not found in registry."),
@ApiResponse(responseCode = "415", description = "Unsupported media type.") })
public Response createFileFormatForSitemaps(final @Context HttpHeaders httpHeaders,
@Parameter(description = "Array of Sitemap UIDs. If empty or omitted, return all Sitemaps from the Registry.") @Nullable List<String> sitemapUIDs) {
String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT);
logger.debug("createFileFormatForSitemaps: mediaType = {}, sitemapUIDs = {}", acceptHeader, sitemapUIDs);
SitemapFileGenerator generator = getSitemapFileGenerator(acceptHeader);
if (generator == null) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity("Unsupported media type '" + acceptHeader + "'!").build();
}
Collection<String> sitemapNames;
Map<String, SitemapProvider> allSitemapNames = sitemapProviders.stream()
.flatMap(provider -> provider.getSitemapNames().stream().map(name -> Map.entry(name, provider)))
.sorted(Comparator.comparing(Map.Entry::getKey))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (existing, replacement) -> existing));
if (sitemapUIDs == null || sitemapUIDs.isEmpty()) {
sitemapNames = allSitemapNames.keySet();
} else if (allSitemapNames.keySet().containsAll(sitemapUIDs)) {
sitemapNames = sitemapUIDs;
} else {
String sitemapUID = sitemapUIDs.stream().filter(name -> !allSitemapNames.keySet().contains(name))
.findFirst().get();
return Response.status(Response.Status.NOT_FOUND)
.entity("Sitemap with UID '" + sitemapUID + "' does not exist!").build();
}
List<Sitemap> sitemaps = sitemapNames.stream().sorted().map(name -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid sorting the names so that that order provided as input is kept.

In case no input is provided, the sitemaps are already sorted by name at line 378.

SitemapProvider provider = allSitemapNames.get(name);
if (provider == null) {
return null;
}
return provider.getSitemap(name);
}).filter(Objects::nonNull).toList();

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
generator.generateFileFormat(outputStream, sitemaps);
return Response.ok(new String(outputStream.toByteArray())).build();
}

/*
* Get all the metadata for a list of items including channel links mapped to metadata in the namespace "channel"
*/
Expand Down Expand Up @@ -465,6 +560,14 @@ private Thing simulateThing(DiscoveryResult result, ThingType thingType) {
};
}

private @Nullable SitemapFileGenerator getSitemapFileGenerator(String mediaType) {
return switch (mediaType) {
case "text/vnd.openhab.dsl.sitemap" -> sitemapFileGenerators.get("DSL");
case "application/yaml" -> sitemapFileGenerators.get("YAML");
default -> null;
};
}

private List<Thing> getThingsOrDiscoveryResult(List<String> thingUIDs) {
return thingUIDs.stream().distinct().map(uid -> {
ThingUID thingUID = new ThingUID(uid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,16 @@
import org.openhab.core.model.sitemap.SitemapProvider;
import org.openhab.core.model.sitemap.sitemap.Button;
import org.openhab.core.model.sitemap.sitemap.ButtonDefinition;
import org.openhab.core.model.sitemap.sitemap.ButtonDefinitionList;
import org.openhab.core.model.sitemap.sitemap.Buttongrid;
import org.openhab.core.model.sitemap.sitemap.Chart;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.ColorArrayList;
import org.openhab.core.model.sitemap.sitemap.Colortemperaturepicker;
import org.openhab.core.model.sitemap.sitemap.Condition;
import org.openhab.core.model.sitemap.sitemap.Frame;
import org.openhab.core.model.sitemap.sitemap.IconRule;
import org.openhab.core.model.sitemap.sitemap.IconRuleList;
import org.openhab.core.model.sitemap.sitemap.Image;
import org.openhab.core.model.sitemap.sitemap.Input;
import org.openhab.core.model.sitemap.sitemap.LinkableWidget;
Expand All @@ -101,6 +104,7 @@
import org.openhab.core.model.sitemap.sitemap.Switch;
import org.openhab.core.model.sitemap.sitemap.Video;
import org.openhab.core.model.sitemap.sitemap.VisibilityRule;
import org.openhab.core.model.sitemap.sitemap.VisibilityRuleList;
import org.openhab.core.model.sitemap.sitemap.Webview;
import org.openhab.core.model.sitemap.sitemap.Widget;
import org.openhab.core.types.State;
Expand Down Expand Up @@ -614,7 +618,8 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
}
bean.widgetId = widgetId;
bean.icon = itemUIRegistry.getCategory(widget);
bean.staticIcon = widget.getStaticIcon() != null || !widget.getIconRules().isEmpty();
bean.staticIcon = widget.getStaticIcon() != null
|| (widget.getIconRules() != null && !widget.getIconRules().getElements().isEmpty());
bean.labelcolor = convertItemValueColor(itemUIRegistry.getLabelColor(widget), itemState);
bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState);
bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState);
Expand Down Expand Up @@ -642,8 +647,8 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
isLeaf(children), uri, locale, false, evenIfHidden);
}
}
if (widget instanceof Switch switchWidget) {
for (Mapping mapping : switchWidget.getMappings()) {
if (widget instanceof Switch switchWidget && switchWidget.getMappings() != null) {
for (Mapping mapping : switchWidget.getMappings().getElements()) {
Copy link

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

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

Potential NullPointerException: getMappings() could return null, but there's no null check before calling getElements(). This should be guarded with a null check like other similar patterns in the codebase.

Copilot uses AI. Check for mistakes.

MappingDTO mappingBean = new MappingDTO();
mappingBean.command = mapping.getCmd();
mappingBean.releaseCommand = mapping.getReleaseCmd();
Expand All @@ -652,8 +657,8 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
bean.mappings.add(mappingBean);
}
}
if (widget instanceof Selection selectionWidget) {
for (Mapping mapping : selectionWidget.getMappings()) {
if (widget instanceof Selection selectionWidget && selectionWidget.getMappings() != null) {
for (Mapping mapping : selectionWidget.getMappings().getElements()) {
Copy link

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

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

Potential NullPointerException: getMappings() could return null, but there's no null check before calling getElements(). This should be guarded with a null check like other similar patterns in the codebase.

Copilot uses AI. Check for mistakes.

MappingDTO mappingBean = new MappingDTO();
mappingBean.command = mapping.getCmd();
mappingBean.label = mapping.getLabel();
Expand Down Expand Up @@ -714,19 +719,23 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
bean.maxValue = colortemperaturepickerWidget.getMaxValue();
}
if (widget instanceof Buttongrid buttonGridWidget) {
for (ButtonDefinition button : buttonGridWidget.getButtons()) {
MappingDTO mappingBean = new MappingDTO();
mappingBean.row = button.getRow();
mappingBean.column = button.getColumn();
mappingBean.command = button.getCmd();
mappingBean.label = button.getLabel();
mappingBean.icon = button.getIcon();
bean.mappings.add(mappingBean);
ButtonDefinitionList buttonDefinitionList = buttonGridWidget.getButtons();
if (buttonDefinitionList != null) {
for (ButtonDefinition button : buttonDefinitionList.getElements()) {
MappingDTO mappingBean = new MappingDTO();
mappingBean.row = button.getRow();
mappingBean.column = button.getColumn();
mappingBean.command = button.getCmd();
mappingBean.label = button.getLabel();
mappingBean.icon = button.getIcon();
bean.mappings.add(mappingBean);
}
}
}
if (widget instanceof Button buttonWidget) {
// Get the icon from the widget only
if (widget.getIcon() == null && widget.getStaticIcon() == null && widget.getIconRules().isEmpty()) {
if (widget.getIcon() == null && widget.getStaticIcon() == null
&& (widget.getIconRules() == null || widget.getIconRules().getElements().isEmpty())) {
bean.icon = null;
bean.staticIcon = null;
}
Expand Down Expand Up @@ -877,26 +886,32 @@ private Set<GenericItem> getAllItems(List<Widget> widgets) {
return items;
}

private Set<GenericItem> getItemsInVisibilityCond(EList<VisibilityRule> ruleList) {
private Set<GenericItem> getItemsInVisibilityCond(@Nullable VisibilityRuleList ruleList) {
Set<GenericItem> items = new HashSet<>();
for (VisibilityRule rule : ruleList) {
getItemsInConditions(rule.getConditions(), items);
if (ruleList != null) {
for (VisibilityRule rule : ruleList.getElements()) {
getItemsInConditions(rule.getConditions(), items);
}
}
return items;
}

private Set<GenericItem> getItemsInColorCond(EList<ColorArray> colorList) {
private Set<GenericItem> getItemsInColorCond(@Nullable ColorArrayList colorList) {
Set<GenericItem> items = new HashSet<>();
for (ColorArray rule : colorList) {
getItemsInConditions(rule.getConditions(), items);
if (colorList != null) {
for (ColorArray rule : colorList.getElements()) {
getItemsInConditions(rule.getConditions(), items);
}
}
return items;
}

private Set<GenericItem> getItemsInIconCond(EList<IconRule> ruleList) {
private Set<GenericItem> getItemsInIconCond(@Nullable IconRuleList ruleList) {
Set<GenericItem> items = new HashSet<>();
for (IconRule rule : ruleList) {
getItemsInConditions(rule.getConditions(), items);
if (ruleList != null) {
for (IconRule rule : ruleList.getElements()) {
getItemsInConditions(rule.getConditions(), items);
}
}
return items;
}
Expand Down
Loading
Loading