Skip to content

Commit

Permalink
added sitemap event subscription mechanism (eclipse-archived#2030)
Browse files Browse the repository at this point in the history
* added sitemap event subscription mechanism

Signed-off-by: Kai Kreuzer <kai@openhab.org>

* allow setting the current page on the initial request and added logging

Signed-off-by: Kai Kreuzer <kai@openhab.org>

* BasicUI: SSE sitemap events support (#5)

Signed-off-by: Vlad Ivanov <vlad-mbx@ya.ru>

* added clean up of closed SSE connections

Signed-off-by: Kai Kreuzer <kai@openhab.org>

* avoided servlet exception on unknown subscription id

Signed-off-by: Kai Kreuzer <kai@openhab.org>

* BasicUI: refactor: move some template processing code to abstract class

Signed-off-by: Vlad Ivanov <vlad-mbx@ya.ru>

* BasicUI: fix widget referencing for SSE stream

Signed-off-by: Vlad Ivanov <vlad-mbx@ya.ru>

* BasicUI: fix icon and value update for some widgets

Signed-off-by: Vlad Ivanov <vlad-mbx@ya.ru>

* BasicUI: remove test logging

Signed-off-by: Vlad Ivanov <vlad-mbx@ya.ru>

* refactored code to avoid duplicate events

Signed-off-by: Kai Kreuzer <kai@openhab.org>

* fixed cleanup of page change listeners

Signed-off-by: Kai Kreuzer <kai@openhab.org>

* also consider change events of group items

Signed-off-by: Kai Kreuzer <kai@openhab.org>
  • Loading branch information
kaikreuzer authored and maggu2810 committed Oct 7, 2016
1 parent 6e3d779 commit 32e5aad
Show file tree
Hide file tree
Showing 35 changed files with 881 additions and 235 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Bundle-SymbolicName: org.eclipse.smarthome.io.rest.sitemap
Bundle-Version: 0.9.0.qualifier
Bundle-Vendor: Eclipse.org
Bundle-RequiredExecutionEnvironment: JavaSE-1.7
Import-Package: io.swagger.annotations;resolution:=optional,
Import-Package: com.google.common.collect,
io.swagger.annotations;resolution:=optional,
javax.servlet,
javax.servlet.http,
javax.ws.rs,
Expand All @@ -21,5 +22,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
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="org.eclipse.smarthome.io.rest.sitemap">
<implementation class="org.eclipse.smarthome.io.rest.sitemap.internal.SitemapResource"/>
<reference bind="setItemUIRegistry" cardinality="1..1" interface="org.eclipse.smarthome.ui.items.ItemUIRegistry" name="ItemUIRegistry" policy="dynamic" unbind="unsetItemUIRegistry"/>
<reference bind="setSitemapSubscriptionService" cardinality="1..1" interface="org.eclipse.smarthome.io.rest.sitemap.SitemapSubscriptionService" name="SitemapSubscriptions" policy="static" unbind="unsetSitemapSubscriptionService"/>
<reference bind="addSitemapProvider" cardinality="0..n" interface="org.eclipse.smarthome.model.sitemap.SitemapProvider" name="SitemapProvider" policy="dynamic" unbind="removeSitemapProvider"/>
<service>
<provide interface="org.eclipse.smarthome.io.rest.sitemap.internal.SitemapResource"/>
<provide interface="org.eclipse.smarthome.io.rest.RESTResource"/>
</service>
<reference bind="addSitemapProvider" cardinality="0..n" interface="org.eclipse.smarthome.model.sitemap.SitemapProvider" name="SitemapProvider" policy="dynamic" unbind="removeSitemapProvider"/>
</scr:component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="org.eclipse.smarthome.io.rest.sitemapsubscription">
<implementation class="org.eclipse.smarthome.io.rest.sitemap.SitemapSubscriptionService"/>
<reference bind="setItemUIRegistry" cardinality="1..1" interface="org.eclipse.smarthome.ui.items.ItemUIRegistry" name="ItemUIRegistry" policy="dynamic" unbind="unsetItemUIRegistry"/>
<reference bind="addSitemapProvider" cardinality="0..n" interface="org.eclipse.smarthome.model.sitemap.SitemapProvider" name="SitemapProvider" policy="dynamic" unbind="removeSitemapProvider"/>
<service>
<provide interface="org.eclipse.smarthome.io.rest.sitemap.SitemapSubscriptionService"/>
</service>
</scr:component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/**
* 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 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 {

private static final String SITEMAP_PAGE_SEPARATOR = "#";

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

public interface SitemapSubscriptionCallback {

void onEvent(SitemapEvent event);
}

private ItemUIRegistry itemUIRegistry;
private List<SitemapProvider> sitemapProviders = new ArrayList<>();

/* subscription id -> sitemap+page */
private final Map<String, String> pageOfSubscription = new ConcurrentHashMap<>();

/* subscription id -> callback */
private Map<String, SitemapSubscriptionCallback> callbacks = new ConcurrentHashMap<>();

/* sitemap+page -> listener */
private Map<String, PageChangeListener> pageChangeListeners = new ConcurrentHashMap<>();

public SitemapSubscriptionService() {
}

protected void activate() {
}

protected void deactivate() {
pageOfSubscription.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);
logger.debug("Created new subscription with id {}", subscriptionId);
return subscriptionId;
}

/**
* Removes an existing subscription
*
* @param subscriptionId the id of the subscription to remove
*/
public void removeSubscription(String subscriptionId) {
callbacks.remove(subscriptionId);
String sitemapPage = pageOfSubscription.remove(subscriptionId);
if (sitemapPage != null && !pageOfSubscription.values().contains(sitemapPage)) {
// this was the only subscription listening on this page, so we can dispose the listener
PageChangeListener listener = pageChangeListeners.get(sitemapPage);
if (listener != null) {
pageChangeListeners.remove(listener);
listener.dispose();
}
}
logger.debug("Removed subscription with id {}", subscriptionId);
}

/**
* 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 pageOfSubscription.get(subscriptionId).split(SITEMAP_PAGE_SEPARATOR)[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 pageOfSubscription.get(subscriptionId).split(SITEMAP_PAGE_SEPARATOR)[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) {
SitemapSubscriptionCallback callback = callbacks.get(subscriptionId);
if (callback != null) {
String oldSitemapPage = pageOfSubscription.remove(subscriptionId);
if (oldSitemapPage != null) {
removeCallbackFromListener(oldSitemapPage, callback);
}
addCallbackToListener(sitemapName, pageId, callback);
pageOfSubscription.put(subscriptionId, getValue(sitemapName, pageId));

logger.debug("Subscription {} changed to page {} of sitemap {}",
new Object[] { subscriptionId, pageId, sitemapName });
} else {
throw new IllegalArgumentException("Subscription " + subscriptionId + " does not exist!");
}
}

private void addCallbackToListener(String sitemapName, String pageId, SitemapSubscriptionCallback callback) {
PageChangeListener listener = pageChangeListeners.get(getValue(sitemapName, pageId));
if (listener == null) {
// there is no listener for this page yet, so let's try to create one
EList<Widget> 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) {
listener = new PageChangeListener(sitemapName, pageId, itemUIRegistry, widgets);
pageChangeListeners.put(getValue(sitemapName, pageId), listener);
}
}
if (listener != null) {
listener.addCallback(callback);
}
}

private void removeCallbackFromListener(String sitemapPage, SitemapSubscriptionCallback callback) {
PageChangeListener oldListener = pageChangeListeners.get(sitemapPage);
if (oldListener != null) {
oldListener.removeCallback(callback);
if (!pageOfSubscription.values().contains(sitemapPage)) {
// no other callbacks are left here, so we can safely dispose the listener
oldListener.dispose();
pageChangeListeners.remove(sitemapPage);
}
}
}

private String getValue(String sitemapName, String pageId) {
return sitemapName + SITEMAP_PAGE_SEPARATOR + pageId;
}

private Sitemap getSitemap(String sitemapName) {
for (SitemapProvider provider : sitemapProviders) {
Sitemap sitemap = provider.getSitemap(sitemapName);
if (sitemap != null) {
return sitemap;
}
}
return null;
}

}
Loading

0 comments on commit 32e5aad

Please sign in to comment.