-
-
Notifications
You must be signed in to change notification settings - Fork 456
Sitemap DSL and YAML serialization #4945
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
base: main
Are you sure you want to change the base?
Changes from all commits
be11ec3
03ba87a
f8550df
f941a0a
0e1892d
4af270e
b95559c
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 |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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 | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please provide the same example in both formats. |
||
version: 2 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
@@ -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(// | ||
|
@@ -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") | ||
|
@@ -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 -> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
*/ | ||
|
@@ -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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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); | ||
|
@@ -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()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Positive FeedbackNegative Feedback |
||
MappingDTO mappingBean = new MappingDTO(); | ||
mappingBean.command = mapping.getCmd(); | ||
mappingBean.releaseCommand = mapping.getReleaseCmd(); | ||
|
@@ -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()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Positive FeedbackNegative Feedback |
||
MappingDTO mappingBean = new MappingDTO(); | ||
mappingBean.command = mapping.getCmd(); | ||
mappingBean.label = mapping.getLabel(); | ||
|
@@ -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; | ||
} | ||
|
@@ -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; | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.
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.
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.