Skip to content

Commit

Permalink
allow subscriptions for the complete sitemap
Browse files Browse the repository at this point in the history
Signed-off-by: Tassilo Karge <[email protected]>
  • Loading branch information
TAKeanice committed Feb 24, 2024
1 parent b64e972 commit 4635625
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
Expand All @@ -29,8 +30,8 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.io.rest.sitemap.internal.PageChangeListener;
import org.openhab.core.io.rest.sitemap.internal.SitemapEvent;
import org.openhab.core.io.rest.sitemap.internal.WidgetsChangeListener;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.events.ItemStatePredictedEvent;
Expand Down Expand Up @@ -72,6 +73,7 @@ public class SitemapSubscriptionService implements ModelRepositoryChangeListener
private static final String SITEMAP_PAGE_SEPARATOR = "#";
private static final String SITEMAP_SUFFIX = ".sitemap";
private static final int DEFAULT_MAX_SUBSCRIPTIONS = 50;
public static final Duration WAIT_AFTER_CREATE_SECONDS = Duration.ofSeconds(30);

private final Logger logger = LoggerFactory.getLogger(SitemapSubscriptionService.class);
private final BundleContext bundleContext;
Expand All @@ -88,7 +90,7 @@ public interface SitemapSubscriptionCallback {
private final List<SitemapProvider> sitemapProviders = new ArrayList<>();

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

/* subscription id -> callback */
private final Map<String, SitemapSubscriptionCallback> callbacks = new ConcurrentHashMap<>();
Expand All @@ -112,7 +114,7 @@ public SitemapSubscriptionService(Map<String, Object> config, final @Reference I

@Deactivate
protected void deactivate() {
pageOfSubscription.clear();
scopeOfSubscription.clear();
callbacks.clear();
creationInstants.clear();
pageChangeListeners.values().forEach(l -> l.serviceRegistration.unregister());
Expand Down Expand Up @@ -176,10 +178,10 @@ protected void removeSitemapProvider(SitemapProvider provider) {
public void removeSubscription(String subscriptionId) {
creationInstants.remove(subscriptionId);
callbacks.remove(subscriptionId);
String sitemapPage = pageOfSubscription.remove(subscriptionId);
if (sitemapPage != null && !pageOfSubscription.containsValue(sitemapPage)) {
String sitemapWithPageId = scopeOfSubscription.remove(subscriptionId);
if (sitemapWithPageId != null && !scopeOfSubscription.containsValue(sitemapWithPageId)) {
// this was the only subscription listening on this page, so we can dispose the listener
ListenerRecord listener = pageChangeListeners.remove(sitemapPage);
ListenerRecord listener = pageChangeListeners.remove(sitemapWithPageId);
if (listener != null) {
listener.serviceRegistration().unregister();
}
Expand All @@ -204,7 +206,7 @@ public boolean exists(String subscriptionId) {
* @return the id of the currently active page or null if no page is currently set for the subscription
*/
public @Nullable String getPageId(String subscriptionId) {
String sitemapWithPageId = pageOfSubscription.get(subscriptionId);
String sitemapWithPageId = scopeOfSubscription.get(subscriptionId);
return (sitemapWithPageId == null) ? null : extractPageId(sitemapWithPageId);
}

Expand All @@ -215,18 +217,39 @@ public boolean exists(String subscriptionId) {
* @return the name of the current sitemap or null if no sitemap is currently set for the subscription
*/
public @Nullable String getSitemapName(String subscriptionId) {
String sitemapWithPageId = pageOfSubscription.get(subscriptionId);
String sitemapWithPageId = scopeOfSubscription.get(subscriptionId);
return (sitemapWithPageId == null) ? null : extractSitemapName(sitemapWithPageId);
}

private String extractSitemapName(String sitemapWithPageId) {
return sitemapWithPageId.split(SITEMAP_PAGE_SEPARATOR)[0];
}

private boolean isPageListener(String sitemapWithPageId) {
return sitemapWithPageId.contains(SITEMAP_PAGE_SEPARATOR);
}

private String extractPageId(String sitemapWithPageId) {
return sitemapWithPageId.split(SITEMAP_PAGE_SEPARATOR)[1];
}

public void unsetPageId(String subscriptionId, String sitemapName) {
SitemapSubscriptionCallback callback = callbacks.get(subscriptionId);
if (callback != null) {
String oldSitemapWithPage = scopeOfSubscription.remove(subscriptionId);
if (oldSitemapWithPage != null) {
removeCallbackFromListener(oldSitemapWithPage, callback);
}
addCallbackToListener(sitemapName, callback);
scopeOfSubscription.put(subscriptionId, getScopeIdentifier(sitemapName));

logger.debug("Subscription {} changed to whole sitemap {} ({} active subscriptions}", subscriptionId,
sitemapName, callbacks.size());
} else {
throw new IllegalArgumentException("Subscription " + subscriptionId + " does not exist!");
}
}

/**
* Updates the subscription to send events for the provided page id.
*
Expand All @@ -237,12 +260,12 @@ private String extractPageId(String sitemapWithPageId) {
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);
String oldSitemapWithPage = scopeOfSubscription.remove(subscriptionId);
if (oldSitemapWithPage != null) {
removeCallbackFromListener(oldSitemapWithPage, callback);
}
addCallbackToListener(sitemapName, pageId, callback);
pageOfSubscription.put(subscriptionId, getValue(sitemapName, pageId));
scopeOfSubscription.put(subscriptionId, getScopeIdentifier(sitemapName, pageId));

logger.debug("Subscription {} changed to page {} of sitemap {} ({} active subscriptions}", subscriptionId,
pageId, sitemapName, callbacks.size());
Expand All @@ -251,18 +274,31 @@ public void setPageId(String subscriptionId, String sitemapName, String pageId)
}
}

private void addCallbackToListener(String sitemapName, SitemapSubscriptionCallback callback) {
String sitemapWithPageId = getScopeIdentifier(sitemapName);
ListenerRecord listener = pageChangeListeners.computeIfAbsent(sitemapWithPageId, v -> {
WidgetsChangeListener newListener = new WidgetsChangeListener(sitemapName, null, itemUIRegistry,
collectAllWidgetsInSitemap(sitemapName));
ServiceRegistration<?> registration = bundleContext.registerService(EventSubscriber.class.getName(),
newListener, null);
return new ListenerRecord(newListener, registration);
});
listener.widgetsChangeListener().addCallback(callback);
}

private void addCallbackToListener(String sitemapName, String pageId, SitemapSubscriptionCallback callback) {
ListenerRecord listener = pageChangeListeners.computeIfAbsent(getValue(sitemapName, pageId), v -> {
PageChangeListener newListener = new PageChangeListener(sitemapName, pageId, itemUIRegistry,
collectWidgets(sitemapName, pageId));
String sitemapWithPageId = getScopeIdentifier(sitemapName, pageId);
ListenerRecord listener = pageChangeListeners.computeIfAbsent(sitemapWithPageId, v -> {
WidgetsChangeListener newListener = new WidgetsChangeListener(sitemapName, pageId, itemUIRegistry,
collectWidgetsOnPage(sitemapName, pageId));
ServiceRegistration<?> registration = bundleContext.registerService(EventSubscriber.class.getName(),
newListener, null);
return new ListenerRecord(newListener, registration);
});
listener.pageChangeListener().addCallback(callback);
listener.widgetsChangeListener().addCallback(callback);
}

private EList<Widget> collectWidgets(String sitemapName, String pageId) {
private EList<Widget> collectWidgetsOnPage(String sitemapName, String pageId) {
EList<Widget> widgets = new BasicEList<>();

Sitemap sitemap = getSitemap(sitemapName);
Expand All @@ -281,19 +317,41 @@ private EList<Widget> collectWidgets(String sitemapName, String pageId) {
return widgets;
}

private EList<Widget> collectAllWidgetsInSitemap(String sitemapName) {
EList<Widget> widgets = new BasicEList<>();
Sitemap sitemap = getSitemap(sitemapName);
if (sitemap != null) {
widgets = itemUIRegistry.getChildren(sitemap);
LinkedList<Widget> childrenQueue = new LinkedList<>(widgets);
while (!childrenQueue.isEmpty()) {
Widget child = childrenQueue.remove(0);
if (child instanceof LinkableWidget) {
List<Widget> subWidgets = itemUIRegistry.getChildren((LinkableWidget) child);
widgets.addAll(subWidgets);
childrenQueue.addAll(subWidgets);
}
}
}
return widgets;
}

private void removeCallbackFromListener(String sitemapPage, SitemapSubscriptionCallback callback) {
ListenerRecord oldListener = pageChangeListeners.get(sitemapPage);
if (oldListener != null) {
oldListener.pageChangeListener().removeCallback(callback);
if (!pageOfSubscription.containsValue(sitemapPage)) {
oldListener.widgetsChangeListener().removeCallback(callback);
if (!scopeOfSubscription.containsValue(sitemapPage)) {
// no other callbacks are left here, so we can safely dispose the listener
oldListener.serviceRegistration().unregister();
pageChangeListeners.remove(sitemapPage);
}
}
}

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

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

Expand All @@ -318,11 +376,16 @@ public void modelChanged(String modelName, EventType type) {
for (Entry<String, ListenerRecord> listenerEntry : pageChangeListeners.entrySet()) {
String sitemapWithPage = listenerEntry.getKey();
String sitemapName = extractSitemapName(sitemapWithPage);
String pageId = extractPageId(sitemapWithPage);

EList<Widget> widgets;
if (sitemapName.equals(changedSitemapName)) {
EList<Widget> widgets = collectWidgets(sitemapName, pageId);
listenerEntry.getValue().pageChangeListener().sitemapContentChanged(widgets);
if (isPageListener(sitemapWithPage)) {
String pageId = extractPageId(sitemapWithPage);
widgets = collectWidgetsOnPage(sitemapName, pageId);
} else {
widgets = collectAllWidgetsInSitemap(sitemapName);
}
listenerEntry.getValue().widgetsChangeListener().sitemapContentChanged(widgets);
}
}
}
Expand All @@ -332,15 +395,16 @@ public void checkAliveClients() {
for (Entry<String, Instant> creationEntry : creationInstants.entrySet()) {
String subscriptionId = creationEntry.getKey();
SitemapSubscriptionCallback callback = callbacks.get(subscriptionId);
if (getPageId(subscriptionId) == null && callback != null
&& (creationEntry.getValue().plus(Duration.ofSeconds(30)).isBefore(Instant.now()))) {
logger.debug("Release subscription {} as sitemap page is not set", subscriptionId);
if (!scopeOfSubscription.containsKey(subscriptionId) && callback != null
&& (creationEntry.getValue().plus(WAIT_AFTER_CREATE_SECONDS).isBefore(Instant.now()))) {
logger.debug("Release subscription {} as it was not queried within {} seconds", subscriptionId,
WAIT_AFTER_CREATE_SECONDS);
removeSubscription(subscriptionId);
callback.onRelease(subscriptionId);
}
}
// Send an ALIVE event to all subscribers to trigger an exception for dead subscribers
pageChangeListeners.values().forEach(l -> l.pageChangeListener().sendAliveEvent());
pageChangeListeners.values().forEach(l -> l.widgetsChangeListener().sendAliveEvent());
}

@Override
Expand All @@ -359,20 +423,21 @@ public void receive(Event event) {
}
for (ListenerRecord listener : pageChangeListeners.values()) {
if (prediction.isConfirmation()) {
listener.pageChangeListener().keepCurrentState(item);
listener.widgetsChangeListener().keepCurrentState(item);
} else {
listener.pageChangeListener().changeStateTo(item, prediction.getPredictedState());
listener.widgetsChangeListener().changeStateTo(item, prediction.getPredictedState());
}
}
} else if (event instanceof ChannelDescriptionChangedEvent channelDescriptionChangedEvent) {
channelDescriptionChangedEvent.getLinkedItemNames().forEach(itemName -> {
for (ListenerRecord listener : pageChangeListeners.values()) {
listener.pageChangeListener().descriptionChanged(itemName);
listener.widgetsChangeListener().descriptionChanged(itemName);
}
});
}
}

private record ListenerRecord(PageChangeListener pageChangeListener, ServiceRegistration<?> serviceRegistration) {
private record ListenerRecord(WidgetsChangeListener widgetsChangeListener,
ServiceRegistration<?> serviceRegistration) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ public class SitemapDTO {

public String link;

public boolean timeout;

public PageDTO homepage;
}
Loading

0 comments on commit 4635625

Please sign in to comment.