diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/META-INF/MANIFEST.MF b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/META-INF/MANIFEST.MF
index 4003f782d62..a8636d15e7b 100644
--- a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/META-INF/MANIFEST.MF
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/META-INF/MANIFEST.MF
@@ -21,5 +21,8 @@ Import-Package: io.swagger.annotations;resolution:=optional,
org.eclipse.smarthome.model.core,
org.eclipse.smarthome.model.sitemap,
org.eclipse.smarthome.ui.items,
+ org.glassfish.jersey.media.sse,
+ org.glassfish.jersey.server,
org.slf4j
-Service-Component: OSGI-INF/sitemaprest.xml
+Service-Component: OSGI-INF/*.xml
+Export-Package: org.eclipse.smarthome.io.rest.sitemap
diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/OSGI-INF/sitemaprest.xml b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/OSGI-INF/sitemaprest.xml
index ccc3b3ce143..d694bda77ed 100644
--- a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/OSGI-INF/sitemaprest.xml
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/OSGI-INF/sitemaprest.xml
@@ -11,9 +11,10 @@
+
+
-
diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/OSGI-INF/sitemapsubscriptionservice.xml b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/OSGI-INF/sitemapsubscriptionservice.xml
new file mode 100644
index 00000000000..57418378fb4
--- /dev/null
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/OSGI-INF/sitemapsubscriptionservice.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/SitemapSubscriptionService.java b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/SitemapSubscriptionService.java
new file mode 100644
index 00000000000..5b1332635c2
--- /dev/null
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/SitemapSubscriptionService.java
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.io.rest.sitemap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.emf.common.util.EList;
+import org.eclipse.smarthome.io.rest.sitemap.internal.PageChangeListener;
+import org.eclipse.smarthome.io.rest.sitemap.internal.SitemapEvent;
+import org.eclipse.smarthome.model.sitemap.LinkableWidget;
+import org.eclipse.smarthome.model.sitemap.Sitemap;
+import org.eclipse.smarthome.model.sitemap.SitemapProvider;
+import org.eclipse.smarthome.model.sitemap.Widget;
+import org.eclipse.smarthome.ui.items.ItemUIRegistry;
+
+/**
+ * This is a service that provides the possibility to manage subscriptions to sitemaps.
+ * As such subscriptions are stateful, they need to be created and removed upon disposal.
+ * The subscription mechanism makes sure that only events for widgets of the currently active sitemap page are sent as
+ * events to the subscriber.
+ * For this to work correctly, the subscriber needs to make sure that setPageId is called whenever it switches to a new
+ * page.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ */
+public class SitemapSubscriptionService {
+
+ public interface SitemapSubscriptionCallback {
+
+ void onEvent(SitemapEvent event);
+ }
+
+ private final Map pageOfSubscriptionMap = new ConcurrentHashMap<>();
+ private ItemUIRegistry itemUIRegistry;
+ private Map callbacks = new ConcurrentHashMap<>();
+ private Map pageChangeListeners = new ConcurrentHashMap<>();
+ private List sitemapProviders = new ArrayList<>();
+
+ public SitemapSubscriptionService() {
+ }
+
+ protected void activate() {
+ }
+
+ protected void deactivate() {
+ pageOfSubscriptionMap.clear();
+ callbacks.clear();
+ for (PageChangeListener listener : pageChangeListeners.values()) {
+ listener.dispose();
+ }
+ pageChangeListeners.clear();
+ }
+
+ protected void setItemUIRegistry(ItemUIRegistry itemUIRegistry) {
+ this.itemUIRegistry = itemUIRegistry;
+ }
+
+ protected void unsetItemUIRegistry(ItemUIRegistry itemUIRegistry) {
+ this.itemUIRegistry = null;
+ }
+
+ protected void addSitemapProvider(SitemapProvider provider) {
+ sitemapProviders.add(provider);
+ }
+
+ protected void removeSitemapProvider(SitemapProvider provider) {
+ sitemapProviders.remove(provider);
+ }
+
+ /**
+ * Creates a new subscription with the given id.
+ *
+ * @param callback an instance that should receive the events
+ * @returns a unique id that identifies the subscription
+ */
+ public String createSubscription(SitemapSubscriptionCallback callback) {
+ String subscriptionId = UUID.randomUUID().toString();
+ callbacks.put(subscriptionId, callback);
+ return subscriptionId;
+ }
+
+ /**
+ * Removes an existing subscription
+ *
+ * @param subscriptionId the id of the subscription to remove
+ */
+ public void removeSubscription(String subscriptionId) {
+ pageOfSubscriptionMap.remove(subscriptionId);
+ callbacks.remove(subscriptionId);
+ PageChangeListener listener = pageChangeListeners.remove(subscriptionId);
+ if (listener != null) {
+ listener.dispose();
+ }
+ }
+
+ /**
+ * Checks whether a subscription with a given id (still) exists.
+ *
+ * @param subscriptionId the id of the subscription to check
+ * @return true, if it exists, false otherwise
+ */
+ public boolean exists(String subscriptionId) {
+ return callbacks.containsKey(subscriptionId);
+ }
+
+ /**
+ * Retrieves the current page id for a subscription.
+ *
+ * @param subscriptionId the subscription to get the page id for
+ * @return the id of the currently active page
+ */
+ public String getPageId(String subscriptionId) {
+ return pageOfSubscriptionMap.get(subscriptionId).split("-")[1];
+ }
+
+ /**
+ * Retrieves the current sitemap name for a subscription.
+ *
+ * @param subscriptionId the subscription to get the sitemap name for
+ * @return the name of the current sitemap
+ */
+ public String getSitemapName(String subscriptionId) {
+ return pageOfSubscriptionMap.get(subscriptionId).split("-")[0];
+ }
+
+ /**
+ * Updates the subscription to send events for the provided page id.
+ *
+ * @param subscriptionId the subscription to update
+ * @param sitemapName the current sitemap name
+ * @param pageId the current page id
+ */
+ public void setPageId(String subscriptionId, String sitemapName, String pageId) {
+ if (exists(subscriptionId)) {
+ pageOfSubscriptionMap.put(subscriptionId, getValue(sitemapName, pageId));
+ removeOldListener(subscriptionId);
+ initNewListener(subscriptionId, sitemapName, pageId);
+ } else {
+ throw new IllegalArgumentException("Subscription " + subscriptionId + " does not exist!");
+ }
+ }
+
+ private void initNewListener(String subscriptionId, String sitemapName, String pageId) {
+ EList widgets = null;
+ Sitemap sitemap = getSitemap(sitemapName);
+ if (sitemap != null) {
+ if (pageId.equals(sitemap.getName())) {
+ widgets = sitemap.getChildren();
+ } else {
+ Widget pageWidget = itemUIRegistry.getWidget(sitemap, pageId);
+ if (pageWidget instanceof LinkableWidget) {
+ widgets = itemUIRegistry.getChildren((LinkableWidget) pageWidget);
+ }
+ }
+ }
+ if (widgets != null) {
+ PageChangeListener listener = new PageChangeListener(sitemapName, pageId, itemUIRegistry, widgets,
+ callbacks.get(subscriptionId));
+ pageChangeListeners.put(subscriptionId, listener);
+ }
+ }
+
+ private void removeOldListener(String subscriptionId) {
+ PageChangeListener oldListener = pageChangeListeners.get(subscriptionId);
+ if (oldListener != null) {
+ oldListener.dispose();
+ }
+ }
+
+ private String getValue(String sitemapName, String pageId) {
+ return sitemapName + "-" + pageId;
+ }
+
+ private Sitemap getSitemap(String sitemapName) {
+ for (SitemapProvider provider : sitemapProviders) {
+ Sitemap sitemap = provider.getSitemap(sitemapName);
+ if (sitemap != null) {
+ return sitemap;
+ }
+ }
+ return null;
+ }
+}
diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/PageChangeListener.java b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/PageChangeListener.java
new file mode 100644
index 00000000000..7d92198222b
--- /dev/null
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/PageChangeListener.java
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.io.rest.sitemap.internal;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.emf.common.util.EList;
+import org.eclipse.smarthome.core.items.GenericItem;
+import org.eclipse.smarthome.core.items.Item;
+import org.eclipse.smarthome.core.items.ItemNotFoundException;
+import org.eclipse.smarthome.core.items.StateChangeListener;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.io.rest.sitemap.SitemapSubscriptionService.SitemapSubscriptionCallback;
+import org.eclipse.smarthome.model.sitemap.Frame;
+import org.eclipse.smarthome.model.sitemap.Widget;
+import org.eclipse.smarthome.ui.items.ItemUIRegistry;
+
+/**
+ * This is a class that listens on item state change events and creates sitemap events for a dedicated sitemap page.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public class PageChangeListener implements StateChangeListener {
+
+ private final String sitemapName;
+ private final String pageId;
+ private final ItemUIRegistry itemUIRegistry;
+ private final EList widgets;
+ private final Set items;
+ private final SitemapSubscriptionCallback callback;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param sitemapName the sitemap name of the page
+ * @param pageId the id of the page for which events are created
+ * @param itemUIRegistry the ItemUIRegistry which is needed for the functionality
+ * @param widgets the list of widgets that are part of the page.
+ * @param callback the instance that should receive the created sitemap events
+ */
+ public PageChangeListener(String sitemapName, String pageId, ItemUIRegistry itemUIRegistry, EList widgets,
+ SitemapSubscriptionCallback callback) {
+ this.sitemapName = sitemapName;
+ this.pageId = pageId;
+ this.itemUIRegistry = itemUIRegistry;
+ this.widgets = widgets;
+ this.callback = callback;
+ items = getAllItems(widgets);
+ for (GenericItem item : items) {
+ item.addStateChangeListener(this);
+ }
+ }
+
+ /**
+ * Disposes this instance and releases all resources.
+ */
+ public void dispose() {
+ for (GenericItem item : items) {
+ item.removeStateChangeListener(this);
+ }
+ }
+
+ /**
+ * 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
+ * @return all items that are represented by the list of widgets
+ */
+ private Set getAllItems(EList widgets) {
+ Set items = new HashSet();
+ if (itemUIRegistry != null) {
+ for (Widget widget : widgets) {
+ String itemName = widget.getItem();
+ if (itemName != null) {
+ try {
+ Item item = itemUIRegistry.getItem(itemName);
+ if (item instanceof GenericItem) {
+ final GenericItem gItem = (GenericItem) item;
+ items.add(gItem);
+ }
+ } catch (ItemNotFoundException e) {
+ // ignore
+ }
+ } else {
+ if (widget instanceof Frame) {
+ items.addAll(getAllItems(((Frame) widget).getChildren()));
+ }
+ }
+ }
+ }
+ return items;
+ }
+
+ @Override
+ public void stateChanged(Item item, State oldState, State newState) {
+ Set events = constructSitemapEvents(item, oldState, newState);
+ for (SitemapEvent event : events) {
+ callback.onEvent(event);
+ }
+ }
+
+ @Override
+ public void stateUpdated(Item item, State state) {
+ }
+
+ private Set constructSitemapEvents(Item item, State oldState, State newState) {
+ Set events = new HashSet<>();
+ for (Widget w : widgets) {
+ if (w.getItem().equals(item.getName())) {
+ SitemapWidgetEvent event = new SitemapWidgetEvent();
+ event.sitemapName = sitemapName;
+ event.pageId = pageId;
+ event.label = itemUIRegistry.getLabel(w);
+ event.labelcolor = itemUIRegistry.getLabelColor(w);
+ event.category = itemUIRegistry.getCategory(w);
+ event.state = newState.toString();
+ event.valuecolor = itemUIRegistry.getValueColor(w);
+ event.widgetId = itemUIRegistry.getWidgetId(w);
+ event.visibility = itemUIRegistry.getVisiblity(w);
+ events.add(event);
+ }
+ }
+ return events;
+ }
+
+}
diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapEvent.java b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapEvent.java
new file mode 100644
index 00000000000..405824d1a63
--- /dev/null
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapEvent.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.io.rest.sitemap.internal;
+
+/**
+ * A general sitemap event, meant to be sub-classed.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ */
+public class SitemapEvent {
+
+ /** The sitemap name this event is for */
+ public String sitemapName;
+
+ /** The page id this event is for */
+ public String pageId;
+
+ public SitemapEvent() {
+ }
+}
diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapEventOutput.java b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapEventOutput.java
new file mode 100644
index 00000000000..5a8f3a6782d
--- /dev/null
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapEventOutput.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.io.rest.sitemap.internal;
+
+import java.io.IOException;
+
+import org.eclipse.smarthome.io.rest.sitemap.SitemapSubscriptionService;
+import org.glassfish.jersey.media.sse.EventOutput;
+import org.glassfish.jersey.media.sse.OutboundEvent;
+
+/**
+ * {@link EventOutput} implementation that takes a subscription id parameter and only writes out events that match the
+ * page of this subscription.
+ * Should only be used when the {@link OutboundEvent}s sent through this {@link EventOutput} contain a data object of
+ * type {@link SitemapEvent}
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public class SitemapEventOutput extends EventOutput {
+
+ private final String subscriptionId;
+ private final SitemapSubscriptionService subscriptions;
+
+ public SitemapEventOutput(SitemapSubscriptionService subscriptions, String subscriptionId) {
+ super();
+ this.subscriptions = subscriptions;
+ this.subscriptionId = subscriptionId;
+ }
+
+ @Override
+ public void write(OutboundEvent chunk) throws IOException {
+ if (chunk.getName().equals("subscriptionId") && chunk.getData().equals(subscriptionId)) {
+ super.write(chunk);
+ } else {
+ SitemapEvent event = (SitemapEvent) chunk.getData();
+ String sitemapName = event.sitemapName;
+ String pageId = event.pageId;
+ if (sitemapName != null && sitemapName.equals(subscriptions.getSitemapName(subscriptionId))
+ && pageId != null && pageId.equals(subscriptions.getPageId(subscriptionId))) {
+ super.write(chunk);
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ subscriptions.removeSubscription(subscriptionId);
+ super.close();
+ }
+}
diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapResource.java b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapResource.java
index 7291cdc49f8..455396c3749 100644
--- a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapResource.java
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapResource.java
@@ -38,9 +38,12 @@
import org.eclipse.smarthome.core.items.ItemNotFoundException;
import org.eclipse.smarthome.core.items.StateChangeListener;
import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.io.rest.JSONResponse;
import org.eclipse.smarthome.io.rest.LocaleUtil;
import org.eclipse.smarthome.io.rest.RESTResource;
import org.eclipse.smarthome.io.rest.core.item.EnrichedItemDTOMapper;
+import org.eclipse.smarthome.io.rest.sitemap.SitemapSubscriptionService;
+import org.eclipse.smarthome.io.rest.sitemap.SitemapSubscriptionService.SitemapSubscriptionCallback;
import org.eclipse.smarthome.model.sitemap.Chart;
import org.eclipse.smarthome.model.sitemap.Frame;
import org.eclipse.smarthome.model.sitemap.Image;
@@ -58,6 +61,10 @@
import org.eclipse.smarthome.model.sitemap.Webview;
import org.eclipse.smarthome.model.sitemap.Widget;
import org.eclipse.smarthome.ui.items.ItemUIRegistry;
+import org.glassfish.jersey.media.sse.EventOutput;
+import org.glassfish.jersey.media.sse.OutboundEvent;
+import org.glassfish.jersey.media.sse.SseBroadcaster;
+import org.glassfish.jersey.media.sse.SseFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -79,7 +86,7 @@
*/
@Path(SitemapResource.PATH_SITEMAPS)
@Api(value = SitemapResource.PATH_SITEMAPS)
-public class SitemapResource implements RESTResource {
+public class SitemapResource implements RESTResource, SitemapSubscriptionCallback {
private final Logger logger = LoggerFactory.getLogger(SitemapResource.class);
@@ -87,13 +94,21 @@ public class SitemapResource implements RESTResource {
private static final long TIMEOUT_IN_MS = 30000;
+ private final SseBroadcaster broadcaster;
+
@Context
UriInfo uriInfo;
private ItemUIRegistry itemUIRegistry;
+ private SitemapSubscriptionService subscriptions;
+
private java.util.List sitemapProviders = new ArrayList<>();
+ public SitemapResource() {
+ this.broadcaster = new SseBroadcaster();
+ }
+
public void setItemUIRegistry(ItemUIRegistry itemUIRegistry) {
this.itemUIRegistry = itemUIRegistry;
}
@@ -102,6 +117,14 @@ public void unsetItemUIRegistry(ItemUIRegistry itemUIRegistry) {
this.itemUIRegistry = null;
}
+ public void setSitemapSubscriptionService(SitemapSubscriptionService subscriptions) {
+ this.subscriptions = subscriptions;
+ }
+
+ public void unsetSitemapSubscriptionService(SitemapSubscriptionService subscriptions) {
+ this.subscriptions = null;
+ }
+
public void addSitemapProvider(SitemapProvider provider) {
sitemapProviders.add(provider);
}
@@ -141,14 +164,24 @@ public Response getSitemapData(@Context HttpHeaders headers,
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Polls the data for a sitemap.", response = PageDTO.class)
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
- @ApiResponse(code = 404, message = "Sitemap with requested name does not exist or page does not exist, or page refers to a non-linkable widget") })
+ @ApiResponse(code = 404, message = "Sitemap with requested name does not exist or page does not exist, or page refers to a non-linkable widget"),
+ @ApiResponse(code = 400, message = "Invalid subscription id has been provided.") })
public Response getPageData(@Context HttpHeaders headers,
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") String language,
@PathParam("sitemapname") @ApiParam(value = "sitemap name") String sitemapname,
- @PathParam("pageid") @ApiParam(value = "page id") String pageId) {
+ @PathParam("pageid") @ApiParam(value = "page id") String pageId,
+ @QueryParam("subscriptionid") @ApiParam(value = "subscriptionid", required = false) String subscriptionId) {
final Locale locale = LocaleUtil.getLocale(language);
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
+ if (subscriptionId != null) {
+ try {
+ subscriptions.setPageId(subscriptionId, sitemapname, pageId);
+ } catch (IllegalArgumentException e) {
+ return JSONResponse.createErrorResponse(Response.Status.BAD_REQUEST, e.getMessage());
+ }
+ }
+
if (headers.getRequestHeader("X-Atmosphere-Transport") != null) {
// Make the REST-API pseudo-compatible with openHAB 1.x
// The client asks Atmosphere for server push functionality,
@@ -159,6 +192,30 @@ public Response getPageData(@Context HttpHeaders headers,
return Response.ok(responseObject).build();
}
+ /**
+ * Subscribes the connecting client to the stream of sitemap events.
+ *
+ * @return {@link EventOutput} object associated with the incoming
+ * connection.
+ */
+ @GET
+ @Path("/subscribe")
+ @Produces(SseFeature.SERVER_SENT_EVENTS)
+ @ApiOperation(value = "Get sitemap events.", response = EventOutput.class)
+ @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
+ public Object getSitemapEvents() {
+ String subscriptionId = subscriptions.createSubscription(this);
+ final EventOutput eventOutput = new SitemapEventOutput(subscriptions, subscriptionId);
+ broadcaster.add(eventOutput);
+
+ // send subscription id as the very first information back
+ OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
+ OutboundEvent outboundEvent = eventBuilder.name("subscriptionId").mediaType(MediaType.TEXT_PLAIN_TYPE)
+ .data(subscriptionId).build();
+ broadcaster.broadcast(outboundEvent);
+ return eventOutput;
+ }
+
private PageDTO getPageBean(String sitemapName, String pageId, URI uri, Locale locale) {
Sitemap sitemap = getSitemap(sitemapName);
if (sitemap != null) {
@@ -543,4 +600,12 @@ public void stateUpdated(Item item, State state) {
}
}
+ @Override
+ public void onEvent(SitemapEvent event) {
+ OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
+ OutboundEvent outboundEvent = eventBuilder.name("event").mediaType(MediaType.APPLICATION_JSON_TYPE).data(event)
+ .build();
+ broadcaster.broadcast(outboundEvent);
+ }
+
}
diff --git a/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapWidgetEvent.java b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapWidgetEvent.java
new file mode 100644
index 00000000000..190f3770206
--- /dev/null
+++ b/bundles/io/org.eclipse.smarthome.io.rest.sitemap/src/main/java/org/eclipse/smarthome/io/rest/sitemap/internal/SitemapWidgetEvent.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.io.rest.sitemap.internal;
+
+/**
+ * A sitemap event, which provides details about a widget that has changed.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ */
+public class SitemapWidgetEvent extends SitemapEvent {
+
+ public String widgetId;
+ public String label;
+ public String state;
+ public String formattedState;
+ public String category;
+ public String labelcolor;
+ public String valuecolor;
+ public boolean visibility;
+
+ public SitemapWidgetEvent() {
+ }
+}
\ No newline at end of file
diff --git a/extensions/ui/org.eclipse.smarthome.ui.basic/META-INF/MANIFEST.MF b/extensions/ui/org.eclipse.smarthome.ui.basic/META-INF/MANIFEST.MF
index de20e90a0f9..317fd072fb0 100644
--- a/extensions/ui/org.eclipse.smarthome.ui.basic/META-INF/MANIFEST.MF
+++ b/extensions/ui/org.eclipse.smarthome.ui.basic/META-INF/MANIFEST.MF
@@ -21,6 +21,7 @@ Import-Package: com.google.gson,
org.eclipse.smarthome.core.transform,
org.eclipse.smarthome.core.types,
org.eclipse.smarthome.io.net.http,
+ org.eclipse.smarthome.io.rest.sitemap,
org.eclipse.smarthome.model.sitemap,
org.eclipse.smarthome.ui.items,
org.osgi.framework,
diff --git a/extensions/ui/org.eclipse.smarthome.ui.basic/OSGI-INF/webappservlet.xml b/extensions/ui/org.eclipse.smarthome.ui.basic/OSGI-INF/webappservlet.xml
index caed82e5e2e..99c11e8a60e 100644
--- a/extensions/ui/org.eclipse.smarthome.ui.basic/OSGI-INF/webappservlet.xml
+++ b/extensions/ui/org.eclipse.smarthome.ui.basic/OSGI-INF/webappservlet.xml
@@ -14,6 +14,7 @@
+
diff --git a/extensions/ui/org.eclipse.smarthome.ui.basic/src/main/java/org/eclipse/smarthome/ui/basic/internal/servlet/WebAppServlet.java b/extensions/ui/org.eclipse.smarthome.ui.basic/src/main/java/org/eclipse/smarthome/ui/basic/internal/servlet/WebAppServlet.java
index f82041fcea1..b5d44a3dc48 100644
--- a/extensions/ui/org.eclipse.smarthome.ui.basic/src/main/java/org/eclipse/smarthome/ui/basic/internal/servlet/WebAppServlet.java
+++ b/extensions/ui/org.eclipse.smarthome.ui.basic/src/main/java/org/eclipse/smarthome/ui/basic/internal/servlet/WebAppServlet.java
@@ -9,8 +9,6 @@
import java.io.IOException;
import java.io.PrintWriter;
-import java.util.Date;
-import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
@@ -21,12 +19,7 @@
import javax.servlet.ServletResponse;
import org.eclipse.emf.common.util.EList;
-import org.eclipse.smarthome.core.items.GenericItem;
-import org.eclipse.smarthome.core.items.Item;
-import org.eclipse.smarthome.core.items.ItemNotFoundException;
-import org.eclipse.smarthome.core.items.StateChangeListener;
-import org.eclipse.smarthome.core.types.State;
-import org.eclipse.smarthome.model.sitemap.Frame;
+import org.eclipse.smarthome.io.rest.sitemap.SitemapSubscriptionService;
import org.eclipse.smarthome.model.sitemap.LinkableWidget;
import org.eclipse.smarthome.model.sitemap.Sitemap;
import org.eclipse.smarthome.model.sitemap.SitemapProvider;
@@ -50,12 +43,6 @@ public class WebAppServlet extends BaseServlet {
private final Logger logger = LoggerFactory.getLogger(WebAppServlet.class);
- /**
- * timeout for polling requests in milliseconds; if no state changes during this time,
- * an empty response is returned.
- */
- private static final long TIMEOUT_IN_MS = 30000L;
-
/** the name of the servlet to be used in the URL */
public static final String SERVLET_NAME = "app";
@@ -63,9 +50,18 @@ public class WebAppServlet extends BaseServlet {
private static final String CONTENT_TYPE = "text/html;charset=UTF-8";
private PageRenderer renderer;
+ private SitemapSubscriptionService subscriptions;
private WebAppConfig config = new WebAppConfig();
protected Set sitemapProviders = new CopyOnWriteArraySet<>();
+ public void setSitemapSubscriptionService(SitemapSubscriptionService subscriptions) {
+ this.subscriptions = subscriptions;
+ }
+
+ public void unsetSitemapSubscriptionService(SitemapSubscriptionService subscriptions) {
+ this.subscriptions = null;
+ }
+
public void addSitemapProvider(SitemapProvider sitemapProvider) {
this.sitemapProviders.add(sitemapProvider);
}
@@ -121,8 +117,8 @@ public void service(ServletRequest req, ServletResponse res) throws ServletExcep
// read request parameters
String sitemapName = req.getParameter("sitemap");
String widgetId = req.getParameter("w");
+ String subscriptionId = req.getParameter("subscriptionId");
boolean async = "true".equalsIgnoreCase(req.getParameter("__async"));
- boolean poll = "true".equalsIgnoreCase(req.getParameter("poll"));
if (sitemapName == null) {
sitemapName = config.getDefaultSitemap();
@@ -147,16 +143,12 @@ public void service(ServletRequest req, ServletResponse res) throws ServletExcep
logger.debug("reading sitemap {}", sitemap.getName());
if (widgetId == null || widgetId.isEmpty() || widgetId.equals(sitemapName)) {
// we are at the homepage, so we render the children of the sitemap root node
+ subscriptions.setPageId(subscriptionId, sitemap.getName(), sitemapName);
String label = sitemap.getLabel() != null ? sitemap.getLabel() : sitemapName;
- EList children = sitemap.getChildren();
- if (poll && waitForChanges(children) == false) {
- // we have reached the timeout, so we do not return any content as nothing has changed
- res.getWriter().append(getTimeoutResponse()).close();
- return;
- }
result.append(renderer.processPage(sitemapName, sitemapName, label, sitemap.getChildren(), async));
} else if (!widgetId.equals("Colorpicker")) {
// we are on some subpage, so we have to render the children of the widget that has been selected
+ subscriptions.setPageId(subscriptionId, sitemap.getName(), widgetId);
Widget w = renderer.getItemUIRegistry().getWidget(sitemap, widgetId);
if (w != null) {
String label = renderer.getItemUIRegistry().getLabel(w);
@@ -167,11 +159,6 @@ public void service(ServletRequest req, ServletResponse res) throws ServletExcep
throw new RenderException("Widget '" + w + "' can not have any content");
}
EList children = renderer.getItemUIRegistry().getChildren((LinkableWidget) w);
- if (poll && waitForChanges(children) == false) {
- // we have reached the timeout, so we do not return any content as nothing has changed
- res.getWriter().append(getTimeoutResponse()).close();
- return;
- }
result.append(renderer.processPage(renderer.getItemUIRegistry().getWidgetId(w), sitemapName, label,
children, async));
}
@@ -188,110 +175,4 @@ public void service(ServletRequest req, ServletResponse res) throws ServletExcep
res.getWriter().close();
}
- /**
- * Defines the response to return on a polling timeout.
- *
- * @return the response of the servlet on a polling timeout
- */
- private String getTimeoutResponse() {
- return "";
- }
-
- /**
- * This method only returns when a change has occurred to any item on the page to display
- *
- * @param widgets the widgets of the page to observe
- */
- private boolean waitForChanges(EList widgets) {
- long startTime = (new Date()).getTime();
- boolean timeout = false;
- BlockingStateChangeListener listener = new BlockingStateChangeListener();
- // let's get all items for these widgets
- Set items = getAllItems(widgets);
- for (GenericItem item : items) {
- item.addStateChangeListener(listener);
- }
- while (!listener.hasChangeOccurred() && !timeout) {
- timeout = (new Date()).getTime() - startTime > TIMEOUT_IN_MS;
- try {
- Thread.sleep(300);
- } catch (InterruptedException e) {
- timeout = true;
- break;
- }
- }
- for (GenericItem item : items) {
- item.removeStateChangeListener(listener);
- }
- return !timeout;
- }
-
- /**
- * Collects all items that are represented by a given list of widgets
- *
- * @param widgets the widget list to get the items for
- * @return all items that are represented by the list of widgets
- */
- private Set getAllItems(EList widgets) {
- Set items = new HashSet();
- if (itemRegistry != null) {
- for (Widget widget : widgets) {
- String itemName = widget.getItem();
- if (itemName != null) {
- try {
- Item item = itemRegistry.getItem(itemName);
- if (item instanceof GenericItem) {
- final GenericItem gItem = (GenericItem) item;
- items.add(gItem);
- }
- } catch (ItemNotFoundException e) {
- // ignore
- }
- } else {
- if (widget instanceof Frame) {
- items.addAll(getAllItems(((Frame) widget).getChildren()));
- }
- }
- }
- }
- return items;
- }
-
- /**
- * This is a state change listener, which is merely used to determine, if a state
- * change has occurred on one of a list of items.
- *
- * @author Kai Kreuzer - Initial contribution and API
- *
- */
- private static class BlockingStateChangeListener implements StateChangeListener {
-
- private boolean changed = false;
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void stateChanged(Item item, State oldState, State newState) {
- changed = true;
- }
-
- /**
- * determines, whether a state change has occurred since its creation
- *
- * @return true, if a state has changed
- */
- public boolean hasChangeOccurred() {
- return changed;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void stateUpdated(Item item, State state) {
- // ignore if the state did not change
- }
- }
-
}