Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions bundles/org.openhab.core.io.rest.core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
<artifactId>org.openhab.core.persistence</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.sitemap</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.discovery</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.util.List;

import org.openhab.core.sitemap.dto.SitemapDefinitionDTO;
import org.openhab.core.thing.dto.ThingDTO;

import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -23,6 +24,7 @@
* in a file format (items, things, ...).
*
* @author Laurent Garnier - Initial contribution
* @author Mark Herwege - Add sitemaps
*/
@Schema(name = "FileFormat")
public class FileFormatDTO {
Expand All @@ -31,4 +33,6 @@ public class FileFormatDTO {
public List<FileFormatItemDTO> items;
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED)
public List<ThingDTO> things;
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED)
public List<SitemapDefinitionDTO> sitemaps;
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@
*/
package org.openhab.core.io.rest.sitemap.internal;

import org.openhab.core.sitemap.dto.AbstractSitemapDTO;

import io.swagger.v3.oas.annotations.media.Schema;

/**
* This is a data transfer object that is used to serialize sitemaps.
*
*
* @author Kai Kreuzer - Initial contribution
* @author Chris Jackson - Initial contribution
* @author Mark Herwege - Moved to abstract class and extend
*/
@Schema(name = "Sitemap")
public class SitemapDTO {

public String name;
public String icon;
public String label;
public class SitemapDTO extends AbstractSitemapDTO {

public String link;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
Expand Down Expand Up @@ -79,7 +81,6 @@
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.sitemap.Button;
import org.openhab.core.sitemap.ButtonDefinition;
import org.openhab.core.sitemap.Buttongrid;
import org.openhab.core.sitemap.Chart;
import org.openhab.core.sitemap.Colortemperaturepicker;
Expand All @@ -88,7 +89,6 @@
import org.openhab.core.sitemap.Image;
import org.openhab.core.sitemap.Input;
import org.openhab.core.sitemap.LinkableWidget;
import org.openhab.core.sitemap.Mapping;
import org.openhab.core.sitemap.Mapview;
import org.openhab.core.sitemap.Parent;
import org.openhab.core.sitemap.Rule;
Expand All @@ -100,8 +100,13 @@
import org.openhab.core.sitemap.Video;
import org.openhab.core.sitemap.Webview;
import org.openhab.core.sitemap.Widget;
import org.openhab.core.sitemap.dto.MappingDTO;
import org.openhab.core.sitemap.dto.SitemapDTOMapper;
import org.openhab.core.sitemap.dto.SitemapDefinitionDTO;
import org.openhab.core.sitemap.registry.SitemapFactory;
import org.openhab.core.sitemap.registry.SitemapRegistry;
import org.openhab.core.types.State;
import org.openhab.core.ui.components.ManagedSitemapProvider;
import org.openhab.core.ui.items.ItemUIRegistry;
import org.openhab.core.ui.items.ItemUIRegistry.WidgetLabelSource;
import org.openhab.core.util.ColorUtil;
Expand All @@ -123,6 +128,7 @@
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;

/**
Expand All @@ -145,6 +151,7 @@
* @author Laurent Garnier - Added support for Buttongrid as container for Button elements
* @author Laurent Garnier - Added support for new sitemap element Colortemperaturepicker
* @author Mark Herwege - Implement sitemap registry, remove Guava dependency
* @author Mark Herwege - Implement sitemap definition endpoints
*/
@Component(service = { RESTResource.class, EventSubscriber.class })
@JaxrsResource
Expand All @@ -166,6 +173,9 @@ public class SitemapResource

private static final long TIMEOUT_IN_MS = 30000;

private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);

private SseBroadcaster<SseSinkInfo> broadcaster;

@Context
Expand All @@ -185,28 +195,31 @@ public class SitemapResource
Sse sse;

private final ItemUIRegistry itemUIRegistry;
private final SitemapFactory sitemapFactory;
private final SitemapRegistry sitemapRegistry;
private final ManagedSitemapProvider managedSitemapProvider;
private final SitemapSubscriptionService subscriptions;
private final LocaleService localeService;
private final TimeZoneProvider timeZoneProvider;

private final WeakValueConcurrentHashMap<String, SseSinkInfo> knownSubscriptions = new WeakValueConcurrentHashMap<>();

private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);

private @Nullable ScheduledFuture<?> cleanSubscriptionsJob;
private Set<BlockingStateChangeListener> stateChangeListeners = new CopyOnWriteArraySet<>();

@Activate
public SitemapResource( //
final @Reference ItemUIRegistry itemUIRegistry, //
final @Reference SitemapFactory sitemapFactory, //
final @Reference SitemapRegistry sitemapRegistry, //
final @Reference ManagedSitemapProvider managedSitemapProvider, //
final @Reference LocaleService localeService, //
final @Reference TimeZoneProvider timeZoneProvider, //
final @Reference SitemapSubscriptionService subscriptions) {
this.itemUIRegistry = itemUIRegistry;
this.sitemapFactory = sitemapFactory;
this.sitemapRegistry = sitemapRegistry;
this.managedSitemapProvider = managedSitemapProvider;
this.localeService = localeService;
this.timeZoneProvider = timeZoneProvider;
this.subscriptions = subscriptions;
Expand Down Expand Up @@ -238,6 +251,115 @@ protected void deactivate() {
broadcaster.close();
}

@GET
@Path("/*/definition")
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "getSitemapDefinitions", summary = "Get all available sitemap definitions.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = SitemapDefinitionDTO.class)))) })
public Response getSitemapsDefinition() {
logger.debug("Received HTTP GET request from IP {} at '{}'", request.getRemoteAddr(), uriInfo.getPath());
Collection<SitemapDefinitionDTO> responseObject = sitemapRegistry.getAll().stream().map(SitemapDTOMapper::map)
.map(this::setIsEditable).toList();
return Response.ok(responseObject).build();
}

@GET
@Path("/{sitemapname: [a-zA-Z_0-9]+}/definition")
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "getSitemapDefinitionByName", summary = "Get sitemap definition by name.", responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SitemapDefinitionDTO.class))),
@ApiResponse(responseCode = "404", description = "Sitemap not found") })
public Response getSitemapDefinition(@Context HttpHeaders headers,
@PathParam("sitemapname") @Parameter(description = "sitemap name") String sitemapname) {
logger.debug("Received HTTP GET request from IP {} at '{}'.", request.getRemoteAddr(), uriInfo.getPath());
Sitemap sitemap = getSitemap(sitemapname);
if (sitemap == null) {
return Response.status(Status.NOT_FOUND).build();
}
SitemapDefinitionDTO responseObject = setIsEditable(SitemapDTOMapper.map(sitemap));
return Response.ok(responseObject).build();
}

private SitemapDefinitionDTO setIsEditable(SitemapDefinitionDTO dto) {
dto.editable = managedSitemapProvider.get(dto.name) != null;
return dto;
}

/**
* Create or Update a sitemap.
*
* @param sitemapname the sitemap name
* @param sitemap data.
* @return Response configured to represent the sitemap depending on the status
*/
@PUT
@RolesAllowed({ Role.ADMIN })
@Path("/{sitemapname: [a-zA-Z_0-9]+}")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(operationId = "addOrUpdateSitemapInRegistry", summary = "Adds a new sitemap to the registry or updates the existing sitemap.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "Sitemap updated.", content = @Content(schema = @Schema(implementation = SitemapDefinitionDTO.class))),
@ApiResponse(responseCode = "201", description = "Sitemap created.", content = @Content(schema = @Schema(implementation = SitemapDefinitionDTO.class))),
@ApiResponse(responseCode = "400", description = "Payload invalid."),
@ApiResponse(responseCode = "405", description = "Sitemap not editable.") })
public Response createOrUpdateSitemap(final @Context HttpHeaders httpHeaders,
@PathParam("sitemapname") @Parameter(description = "sitemap name") String sitemapName,
@Parameter(description = "sitemap data", required = true) @Nullable SitemapDefinitionDTO sitemapDTO) {
// If we didn't get a sitemap, then return!
if (sitemapDTO == null) {
return Response.status(Status.BAD_REQUEST).build();
} else if (!sitemapName.equals((sitemapDTO.name))) {
logger.warn(
"Received HTTP PUT request at '{}' with a sitemap name '{}' that does not match the one in the url.",
uriInfo.getPath(), sitemapDTO.name);
return Response.status(Status.BAD_REQUEST).build();
}

try {
Sitemap sitemap = SitemapDTOMapper.map(sitemapDTO, sitemapFactory);

// Save the sitemap
if (getSitemap(sitemapName) == null) {
// sitemap does not yet exist, create it
managedSitemapProvider.add(sitemap);
return JSONResponse.createResponse(Status.CREATED, sitemapDTO, null);
} else if (managedSitemapProvider.get(sitemapName) != null) {
// sitemap already exists as a managed sitemap, update it
managedSitemapProvider.update(sitemap);
return JSONResponse.createResponse(Status.OK, SitemapDTOMapper.map(sitemap), null);
} else {
// Sitemap exists but cannot be updated
logger.warn("Cannot update existing sitemap '{}', because is not managed.", sitemapName);
return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED,
"Cannot update unmanaged sitemap " + sitemapName);
}
} catch (IllegalArgumentException e) {
logger.warn("Received HTTP PUT request at '{}' with an invalid sitemap name '{}'.", uriInfo.getPath(),
sitemapDTO.name);
return Response.status(Status.BAD_REQUEST).build();
}
}

@DELETE
@RolesAllowed({ Role.ADMIN })
@Path("/{sitemapname: [a-zA-Z_0-9]+}")
@Operation(operationId = "removeSitemapFromRegistry", summary = "Removes a sitemap from the registry.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "404", description = "Sitemap not found."),
@ApiResponse(responseCode = "405", description = "Sitemap not editable.") })
public Response removeSitemap(
@PathParam("sitemapname") @Parameter(description = "sitemap name") String sitemapName) {
if (getSitemap(sitemapName) == null) {
return Response.status(Status.NOT_FOUND).build();
}
if (managedSitemapProvider.remove(sitemapName) == null) {
return JSONResponse.createErrorResponse(Status.METHOD_NOT_ALLOWED,
"Cannot delete unmanaged sitemap " + sitemapName);
}
return Response.ok(null, MediaType.TEXT_PLAIN).build();
}

@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "getSitemaps", summary = "Get all available sitemaps.", responses = {
Expand All @@ -256,11 +378,9 @@ public Response getSitemaps() {
public Response getSitemapData(@Context HttpHeaders headers,
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
@PathParam("sitemapname") @Parameter(description = "sitemap name") String sitemapname,
@QueryParam("type") String type, @QueryParam("jsoncallback") @DefaultValue("callback") String callback,
@QueryParam("includeHidden") @Parameter(description = "include hidden widgets") boolean includeHiddenWidgets) {
final Locale locale = localeService.getLocale(language);
logger.debug("Received HTTP GET request from IP {} at '{}' for media type '{}'.", request.getRemoteAddr(),
uriInfo.getPath(), type);
logger.debug("Received HTTP GET request from IP {} at '{}'.", request.getRemoteAddr(), uriInfo.getPath());
URI uri = uriInfo.getBaseUriBuilder().build();
SitemapDTO responseObject = getSitemapBean(sitemapname, uri, locale, includeHiddenWidgets, false);
return Response.ok(responseObject).build();
Expand Down Expand Up @@ -620,22 +740,22 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
}
}
if (widget instanceof Switch switchWidget) {
for (Mapping mapping : switchWidget.getMappings()) {
bean.mappings = switchWidget.getMappings().stream().map(mapping -> {
MappingDTO mappingBean = new MappingDTO();
mappingBean.command = mapping.getCmd();
mappingBean.releaseCommand = mapping.getReleaseCmd();
mappingBean.label = mapping.getLabel();
mappingBean.icon = mapping.getIcon();
bean.mappings.add(mappingBean);
}
return mappingBean;
}).toList();
}
if (widget instanceof Selection selectionWidget) {
for (Mapping mapping : selectionWidget.getMappings()) {
bean.mappings = selectionWidget.getMappings().stream().map(mapping -> {
MappingDTO mappingBean = new MappingDTO();
mappingBean.command = mapping.getCmd();
mappingBean.label = mapping.getLabel();
bean.mappings.add(mappingBean);
}
return mappingBean;
}).toList();
}
if (widget instanceof Input inputWidget) {
bean.inputHint = inputWidget.getInputHint();
Expand Down Expand Up @@ -692,15 +812,15 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
bean.maxValue = colortemperaturepickerWidget.getMaxValue();
}
if (widget instanceof Buttongrid buttonGridWidget) {
for (ButtonDefinition button : buttonGridWidget.getButtons()) {
bean.mappings = buttonGridWidget.getButtons().stream().map(button -> {
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);
}
return mappingBean;
}).toList();
}
if (widget instanceof Button buttonWidget) {
// Get the icon from the widget only
Expand Down Expand Up @@ -778,8 +898,7 @@ private boolean blockUntilChangeOccurs(String sitemapname, @Nullable String page
* This method only returns when a change has occurred to any item on the
* page to display or if the timeout is reached
*
* @param widgets
* the widgets of the page to observe
* @param widgets the widgets of the page to observe
* @return true if the timeout is reached
*/
private boolean waitForChanges(List<Widget> widgets) {
Expand Down Expand Up @@ -808,8 +927,7 @@ private boolean waitForChanges(List<Widget> widgets) {
/**
* Collects all items that are represented by a given list of widgets
*
* @param widgets
* the widget list to get the items for added to all bundles containing REST resources
* @param widgets the widget list to get the items for added to all bundles containing REST resources
* @return all items that are represented by the list of widgets
*/
private Set<GenericItem> getAllItems(List<Widget> widgets) {
Expand Down
Loading
Loading