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 - } - } - }