![JAR search and dependency download from the Maven repository](/logo.png)
com.pushtechnology.diffusion.examples.ControlClientTopicNotifications Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (C) 2017, 2023 DiffusionData Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.pushtechnology.diffusion.examples;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.pushtechnology.diffusion.client.Diffusion;
import com.pushtechnology.diffusion.client.callbacks.Registration;
import com.pushtechnology.diffusion.client.features.control.topics.TopicNotifications;
import com.pushtechnology.diffusion.client.features.control.topics.TopicNotifications.NotificationRegistration;
import com.pushtechnology.diffusion.client.features.control.topics.TopicNotifications.TopicNotificationListener;
import com.pushtechnology.diffusion.client.session.Session;
import com.pushtechnology.diffusion.client.topics.details.TopicSpecification;
/**
* Examples for using the {@link TopicNotifications} feature to receive
* notifications for the topic tree.
*
* @author DiffusionData Limited
* @since 6.0
*/
public class ControlClientTopicNotifications {
private final Session session;
private final TopicNotifications notifications;
/**
* Constructor.
*
*/
public ControlClientTopicNotifications() {
this.session =
Diffusion.sessions().principal("control")
.password("password")
.open("ws://diffusion.example.com:80");
this.notifications = session.feature(TopicNotifications.class);
}
/**
* Basic example of receiving notifications about all topics under a given
* branch. This registers a long-lived notification listener and selects all
* topics under the provided root path.
*
* With topics at the following paths:
*
*
* a
* a/b
* a/b/c/d
*
* e/f
* e/f/g/h/i
*
*
* To listen for topic events:
*
*
* TreeListener listener = new TreeListener() {
*
* public void onTopicAdded(String topicPath,
* TopicSpecification specification) {
* // Handle topic addition
* }
*
* public void onTopicRemoved(String topicPath,
* TopicSpecification specification) {
* // Handle topic removal
* }
*
* public void onClose() {
* // The listener has been closed
* }
* };
*
* // Get notifications of all topics at or below the topic path "a"
* topicsForBranch("a", listener);
*
* // Get notifications of all topics at or below the topic path "e"
* topicsForBranch("e", listener);
*
*
* which would result in each topic path and specification being notified to
* {@link ControlClientTopicNotifications.TreeListener#onTopicAdded(String, TopicSpecification)
* onTopicAdded}.
*
* @param rootPath the topic path from which to start walking the tree
* @param listener the listener on which to receive tree structure callbacks
* @return a closeable allowing the cancellation of the registered listener
*/
public Closeable topicsForBranch(String rootPath, TreeListener listener) {
final CompletableFuture registration =
notifications.addListener(
new TopicNotificationListener.Default() {
@Override
public void onTopicNotification(String topicPath,
TopicSpecification spec, NotificationType type) {
switch (type) {
case ADDED:
case SELECTED:
listener.onTopicAdded(topicPath, spec);
return;
case REMOVED:
case DESELECTED:
listener.onTopicRemoved(topicPath, spec);
return;
default:
// No default action
}
}
@Override
public void onClose() {
listener.onClose();
}
});
registration.thenAccept(r -> {
r.select(rootPath + "//");
});
return new Closeable() {
@Override
public void close() {
registration.thenAccept(Registration::close);
}
};
}
/**
* Listener for receiving callbacks about the structure of the topic tree.
*/
interface TreeListener {
/**
* Notification that this listener has been closed.
*/
void onClose();
/**
* Notification that a topic has been added.
*
* @param topicPath the topic path
* @param specification the topic specification
*/
void onTopicAdded(String topicPath, TopicSpecification specification);
/**
* Notification that a topic has been removed.
*
* @param topicPath the topic path
* @param specification the topic specification
*/
void onTopicRemoved(String topicPath, TopicSpecification specification);
}
/**
* Advanced example of walking the topic tree from a given root path in
* conjunction with a GUI system, with each topic that the GUI is notified
* about providing a means to descend further down the topic tree in
* reaction to user-controlled events.
*
* With topics at the following paths:
*
*
* a
* a/b
* a/b/c/d
* a/e
* a/e/f
*
*
* Running the following code will result in the GUI being notified of topic
* {@code "a"}:
*
*
* // Some abstract GUI reference
* GUI gui = new GUI() {
* void onTopicAdded(
* String topicPath,
* TopicSpecification specification,
* Consumer<Boolean> selectDescendants) {
* ...
* }
*
* void onTopicRemoved(String topicPath) {
* ...
* }
* };
*
* walkTree("a", new TreeWalker() {
* void onTopicAdded(
* TreeNode node,
* TopicSpecification specification) {
* // Add a selected topic and its specification to the GUI,
* // along with a function to toggle selection of
* // the node's immediate descendants
* gui.onTopicAdded(
* node.getTopicPath(),
* specification,
* node::selectDescendants);
* }
*
* void onTopicRemoved(TreeNode node) {
* // Remove a selected topic from the GUI
* gui.onTopicRemoved(node.getTopicPath());
* }
* });
*
*
* Once the callback provided to the GUI to select descendants is invoked
* (such as by a user clicking to 'expand' the tree), the walker will be
* notified of topics {@code "a/b"} and {@code "a/e"}. For each of those
* topics, the GUI can choose to select the descendants of that path. When
* the GUI decides to expand {@code "a/b"}, it will then be informed of
* topic {@code "a/b/c/d"}.
*
* When the returned {@code Closeable} is invoked to close the walker, the
* GUI will be informed of the removal of all topics. No further
* interactions will occur.
*
* @param rootPath the root topic path from which to begin walking the topic
* tree
* @param walker the tree walker to receive topic notifications
* @return a closeable to allow deregistration of the tree walker
* @throws Exception
*/
public Closeable walkTree(String rootPath, TreeWalker walker)
throws Exception {
final InternalListener listener =
new InternalListener(rootPath, walker);
final CompletableFuture registration =
notifications.addListener(listener);
registration.thenAccept(listener::initialise);
return new Closeable() {
@Override
public void close() throws IOException {
registration.thenAccept(Registration::close);
}
};
}
/**
* Callback interface for walking the topic tree via {@link #walkTree}.
*/
interface TreeWalker {
/**
* Notification that a topic has been added.
*
* @param node the tree node for this topic
* @param specification the topic specification for this topic
*/
void onTopicAdded(TreeNode node, TopicSpecification specification);
/**
* Notification that a topic has been removed.
*
* @param node the tree node for this topic
*/
void onTopicRemoved(TreeNode node);
}
/**
* Representation of a bound topic path, providing navigation operations for
* walking further down the tree.
*/
interface TreeNode {
/**
* @return the topic path that this node references
*/
String getTopicPath();
/**
* Toggle whether immediate descendants of this topic node should be
* selected or not.
*
* @param select {@code true} if this node should select immediate
* descendants
*/
void selectDescendants(boolean select);
}
/**
* An internal implementation of TopicNotificationListener, providing a
* means of registering new listeners for each exposed TreeNode, and
* maintaining references to child nodes that have been created.
*/
private static class InternalListener
extends TopicNotificationListener.Default {
private final ConcurrentMap topicNodes =
new ConcurrentHashMap<>();
private volatile NotificationRegistration registration;
private final String rootPath;
private final TreeWalker walker;
/**
* Constructor.
*
* @param rootPath the root path
* @param walker the associated tree walker
*/
InternalListener(String rootPath, TreeWalker walker) {
this.rootPath = rootPath;
this.walker = walker;
}
public void initialise(NotificationRegistration newRegistration) {
registration = newRegistration;
registration.select(rootPath);
}
@Override
public void onTopicNotification(
String topicPath,
TopicSpecification specification,
NotificationType type) {
if (type == NotificationType.ADDED ||
type == NotificationType.SELECTED) {
final InternalTreeNode node =
new InternalTreeNode(registration, topicPath);
topicNodes.put(topicPath, node);
walker.onTopicAdded(node, specification);
}
else {
walker.onTopicRemoved(topicNodes.remove(topicPath));
}
}
@Override
public void onDescendantNotification(
String topicPath,
NotificationType type) {
InternalTreeNode parent = null;
String path = topicPath;
// Walk up the path until we find the closest registered topic
while (true) {
final int index = path.lastIndexOf("/");
if (index == -1) {
break;
}
path = path.substring(0, index);
parent = topicNodes.get(path);
if (parent != null) {
break;
}
}
// If we don't have any parent nodes, then we're at the root path -
// so just directly select it
if (parent == null) {
registration.select(topicPath);
}
// Otherwise, add to the registered node, and let the 'select
// descendants' toggle determine if we select
else if (type == NotificationType.ADDED ||
type == NotificationType.SELECTED) {
parent.addDescendant(topicPath);
}
else {
parent.removeDescendant(topicPath);
}
}
@Override
public void onClose() {
final List pathsToRemove =
new ArrayList<>(topicNodes.keySet());
// Sort by length – longer paths are lower in the topic tree,
// so we remove topics by walking up the
// tree
pathsToRemove.sort((s1, s2) -> s1.length() > s2.length() ? -1 :
s2.length() > s1.length() ? 1 : 0);
for (String topicPath : pathsToRemove) {
walker.onTopicRemoved(topicNodes.remove(topicPath));
}
}
private class InternalTreeNode implements TreeNode {
// Maintain a list of all unselected topics below this node
private final List descendants = new ArrayList<>();
private final NotificationRegistration registration;
private final String topicPath;
private boolean selectsDescendants = false;
InternalTreeNode(
NotificationRegistration registration,
String topicPath) {
this.registration = registration;
this.topicPath = topicPath;
}
@Override
public String getTopicPath() {
return topicPath;
}
@Override
public synchronized void selectDescendants(boolean selects) {
selectsDescendants = selects;
if (selectsDescendants) {
for (String descendant : descendants) {
registration.select(descendant);
}
}
}
public synchronized void addDescendant(String descendantPath) {
descendants.add(descendantPath);
if (selectsDescendants) {
registration.select(descendantPath);
}
}
public synchronized void removeDescendant(String descendantPath) {
descendants.remove(descendantPath);
if (selectsDescendants) {
registration.deselect(descendantPath);
}
}
}
}
}