/**
* Copyright (c) 2014-2017 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.Map.Entry;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang.StringUtils;
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.core.EventType;
import org.eclipse.smarthome.model.core.ModelRepository;
import org.eclipse.smarthome.model.core.ModelRepositoryChangeListener;
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 implements ModelRepositoryChangeListener {
private static final String SITEMAP_PAGE_SEPARATOR = "#";
private static final String SITEMAP_SUFFIX = ".sitemap";
private final Logger logger = LoggerFactory.getLogger(SitemapSubscriptionService.class);
public interface SitemapSubscriptionCallback {
void onEvent(SitemapEvent event);
}
private ItemUIRegistry itemUIRegistry;
private ModelRepository modelRepo;
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);
}
protected void addModelRepository(ModelRepository modelRepo) {
this.modelRepo = modelRepo;
this.modelRepo.addModelRepositoryChangeListener(this);
}
protected void removeModelRepository(ModelRepository modelRepo) {
this.modelRepo.removeModelRepositoryChangeListener(this);
this.modelRepo = null;
}
/**
* 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.remove(sitemapPage);
if (listener != null) {
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 extractPageId(pageOfSubscription.get(subscriptionId));
}
/**
* 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 extractSitemapName(pageOfSubscription.get(subscriptionId));
}
private String extractSitemapName(String sitemapWithPageId) {
return sitemapWithPageId.split(SITEMAP_PAGE_SEPARATOR)[0];
}
private String extractPageId(String sitemapWithPageId) {
return sitemapWithPageId.split(SITEMAP_PAGE_SEPARATOR)[1];
}
/**
* 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;
widgets = collectWidgets(sitemapName, pageId);
if (widgets != null) {
listener = new PageChangeListener(sitemapName, pageId, itemUIRegistry, widgets);
pageChangeListeners.put(getValue(sitemapName, pageId), listener);
}
}
if (listener != null) {
listener.addCallback(callback);
}
}
private EList<Widget> collectWidgets(String sitemapName, String pageId) {
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);
}
}
}
return widgets;
}
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;
}
@Override
public void modelChanged(String modelName, EventType type) {
if (type != EventType.MODIFIED || !modelName.endsWith(SITEMAP_SUFFIX)) {
return; // we process only sitemap modifications here
}
String changedSitemapName = StringUtils.removeEnd(modelName, SITEMAP_SUFFIX);
for (Entry<String, PageChangeListener> listenerEntry : pageChangeListeners.entrySet()) {
String sitemapWithPage = listenerEntry.getKey();
String sitemapName = extractSitemapName(sitemapWithPage);
String pageId = extractPageId(sitemapWithPage);
if (sitemapName.equals(changedSitemapName)) {
EList<Widget> widgets = collectWidgets(sitemapName, pageId);
listenerEntry.getValue().sitemapContentChanged(widgets);
}
}
}
}