Custom subscriptions
Brightspot comes with subscriptions for common events such as publishing an item or mentions in a conversation. You can define custom subscriptions for other events that occur in your version of Brightspot. These events include actions within Brightspot itself or events occurring in websites published with Brightspot, such as a visitor logging in. As long as you are able to detect the events via code, you can hook them into the notification system and deliver corresponding messages to recipients.
Step 1: Declaring event context
Before creating your own subscription class, you must first determine the event’s context—what type of object will be used to capture the data about the event. The context is used downstream by the message formatters to actually generate the notification. The subscription class must extend from Recordable so that the context can be serialized and saved in the database. Sometimes you can leverage an existing content type for this, but more often you will want to define your own custom type so that you can capture exactly the right data.
For an example of declaring a context for capturing login or logout events, see the snippet "Class for storing the context of a login-logout event."
Step 2: Declaring subscriptions
A subscription class extends from the abstract class ToolSubscription<C>, where C is the type of context declared in Step 1: Declaring event context. Your class should contain any fields or methods relevant to configuring how and when a notification is triggered for this subscription. Users will configure their own subscriptions using those fields.
There are four methods inherited from Subscription you should override in your implementation.
1abstract class ToolSubscription<C> {23protected boolean shouldDeliver(Receiver receiver, C context);45protected boolean overlaps(Subscription<?> other);67public abstract String toStringFormat(Receiver receiver, C context);89public abstract String toHtmlFormat(Receiver receiver, C context);10}
The optional method shouldDeliver is your final opportunity to decide whether or not the given Receiver should actually be notified based on the associated Subscription and Context. The method returns true to send the message, and false to not send the message. For example, if a context’s time stamp is outside a certain range, you may not want to send the notification; in that case this method returns false.
The optional method overlaps evaluates if a user configures subscriptions that cause more than one notification for the same event. If the result is true, Brightspot displays a message during the subscription setup phase that the current configuration results in multiple notifications for the same event or action.
The methods toStringFormat and toHtmlFormat are required for every subscription. They serve as the default message payload for your notification. Both methods are passed the receiver of the notification (usually a ToolUser), and the context (the data about the event or action). Because you have access to the receiver and any of the subscription’s configuration fields, you can create very personalized notifications. The toStringFormat method should return a plain text representation of notification, and the toHtmlFormat method should return a rich-text markup representation of your notification. (In this context, “rich text” includes the special tags and attributes that, when detected, will automatically be converted into Brightspot links. For more information about this format, see Formatting notification messages.) These methods should never return null; use the method shouldDeliver to determine if one of the methods will return null, and then you can avoid sending the message altogether.
For an example of a custom subscription, see the snippet "Class for configuring a subscription for login/logout events."
Step 3: Declaring publishers
When an event is triggered, a publisher is created with a reference to the event. The publisher triggers all of the notifications to be sent for a specific type of subscription.
The publisher is also (optionally) responsible for determining the baseline set of receivers that are eligible to receive this type of notification. While this is ultimately controlled by the Subscription#shouldDeliver method, giving the publisher the first say can serve as both a performance optimization as well as a means to make the system more flexible so that subscriptions can be re-used outside of the context of just Brightspot users.
To create a custom publisher, extend the abstract class Publisher<S extends Subscription<C>, C extends Recordable>. Overriding the parent constructor and calling super ensures that the publisher can be initialized with the context. If you do not override any other methods, then Brightspot will query all receiver objects in the database and process their subscriptions to see if a notification should be sent. You can improve and customize this process by overriding one of the following receiver-related methods.
1abstract class Publisher<S extends Subscription<C>, C extends Recordable> {23protected Class<? extends Receiver> getReceiverType();45protected Query<? extends Receiver> getReceiversQuery();67protected Iterable<? extends Receiver> getReceivers();89}
The method getReceiverType returns the class representing a receiver. The default implementation returns Receiver.class. You can override this method to return a subclass of this class, which may provide some optimization.
The method getReceiversQuery is the default query for determining the baseline set of receivers. This query returns all receivers of the type returned from getReceiverType. You can override this method to return a query that retrieves an optimized baseline set of receivers. For more information about composing a query, see Querying.
The method getReceivers executes the query returned from getReceiversQuery, returning all of the records matching the query. You can override this method so that the query returns a limited number of matching records. For more information about executing a query, see Querying.
For an example of overriding these methods, see the snippet "Class declaring a publisher for subscriptions for login/logout events."
Step 4: Detecting an event
The typical way to detect an event is to trap one of the events in the persistence life cycle, such as beforeSave or afterDelete. As these methods are available in the Record class, you typically use Modification<T> to access those methods.
For an example of detecting an event, see the snippet "Class for trapping login/logout event and launching associated publisher."
See also:
Step 5: Invoking the publisher
The final step is to actually detect when your event occurs, instantiate your publisher with the context, and invoke one of the following publish notification methods.
1abstract class Publisher<S extends Subscription<C>, C extends Recordable> {23public final Notification publishNotification();45public static <S extends Subscription<C>,6C extends Recordable> Notification7publishNotification (Class<S> subscriptionClass, C context);89public final void publishNotificationAsync(Consumer<Notification> callback);1011}
The method publishNotification runs synchronously, and returns a notification once all messages have been delivered. The notification acts as a receipt for the previous operation. It contains data such as when the notification was published, the context, and a list of all the receivers along with their matching subscription and delivery-option preferences. Use the method publishNotification if the event was triggered from a background thread already, as it can be a long running operation.
The method publishNotificationAsync is the asynchronous version of publishNotification. Use this method if your event is triggered from a UI thread. The method returns immediately, and queues up the publish operation in a background thread. You can pass to this method a callback Consumer<Notification> that runs when the operation completes.
If there are matching subscriptions and at least one delivery attempt made, then the notification is saved in the database and you can query for it as needed. If the publish action yields no messages to be delivered, the notification object is created but it is not saved in the database.
Code sample—detecting and notifying about user login and logout events
This sample describes how to create a subscription that sends notifications whenever a user logs in or out of Brightspot. The notification includes the user’s name as well as the action (login or logout).
The following snippet declares the context for a user’s login-logout event. The context includes the user and the action the user performed (login or logout). Context classes must extend from Record.
1import com.psddev.cms.db.ToolUser;2import com.psddev.dari.db.Record;34public class ToolUserAuthEvent extends Record {56/* The user who performed some action. */7private ToolUser user;89/* The action performed by the user. A separate enum lists10the two actions, login and logout. */11private ToolUserAuthAction action;1213/* Cache of the user's label in case the reference is deleted in the future. */14private String userLabel;1516/* A no-arg constructor is required by Dari for Record objects. */17public ToolUserAuthEvent() {18}1920/* Creates a new event for the given user and action. */21ToolUserAuthEvent(ToolUser user, ToolUserAuthAction action) {22this.user = user;23this.action = action;24this.userLabel = user.getLabel();25}2627/* Gets the user who performed the action. */28ToolUser getUser() {29return user;30}3132/* Gets the action (login or logout) */33ToolUserAuthAction getAction() {34return action;35}3637/* Gets the user's label */38public String getUserLabel() {39return user != null ? user.getLabel() : userLabel;40}41}
The following snippet declares a subscription to receive notifications when a user attempts to log in or log out. The subscription includes two fields: a set of users and a set of actions. These fields appear in the UI, and subscribers use these fields to configure the users and actions (login or logout) for which they want to receive notifications. If these fields are blank, Brightspot sends notifications for all users and all actions.
1import com.psddev.cms.db.ToolUi;2import com.psddev.cms.db.ToolUser;3import com.psddev.cms.notification.Receiver;4import com.psddev.cms.notification.Subscription;5import com.psddev.cms.notification.ToolSubscription;6import com.psddev.dari.util.HtmlWriter;7import com.psddev.dari.util.ObjectUtils;8import org.jsoup.Jsoup;910public final class ToolUserAuthSubscription extends ToolSubscription<ToolUserAuthEvent> {1112/* Send a notification if one of the selected users performs some13action. If the field is left blank, notify for all users. */14@ToolUi.DropDown15@ToolUi.Placeholder("All Users")16private Set<ToolUser> users;1718/* Send a notification if one of the selected actions is detected. If19the field is left blank, notify on all actions. */20@ToolUi.Placeholder("All Actions")21private Set<ToolUserAuthAction> actions;2223/* Deliver the notification if the user and action is contained in the set of24users and actions defined by this Subscription. */25@Override26protected boolean shouldDeliver(Receiver receiver, ToolUserAuthEvent authEvent) {27ToolUser user = authEvent.getUser();28ToolUserAuthAction action = authEvent.getAction();29return (ObjectUtils.isBlank(users) || users.contains(user))30&& (ObjectUtils.isBlank(this.actions) || actions.contains(action));31}3233/* Format the notification as rich text markup that can be34transformed into many different formats. */35@Override36protected String toHtmlFormat(Receiver receiver, ToolUserAuthEvent authEvent) {3738ToolUser user = authEvent.getUser();39ToolUserAuthAction action = authEvent.getAction();40String userLabel = authEvent.getUserLabel();4142StringWriter html = new StringWriter();43HtmlWriter writer = new HtmlWriter(html);4445try {46writer.writeHtml(action);47writer.writeHtml(" detected from ");4849/* The content tag and data-id attribute serve as a way to tell50downstream systems that the body of the element represents a51label for some content that lives in the database, in this case,52a Brightspot user. This can be transformed. */53writer.writeStart("content", "data-id", user != null ? user.getId() : null);54{55writer.writeHtml(userLabel);56}57writer.writeEnd();5859writer.writeHtml(".");6061} catch (IOException e) {62throw new IllegalStateException(e);63}6465return html.toString();66}6768/* The notification formatted as plain text. */69@Override70protected String toStringFormat(Receiver receiver, ToolUserAuthEvent authEvent) {71/* Delegate to the toHtmlFormat method and strip out any markup. */72return Jsoup.parse(toHtmlFormat(receiver, authEvent)).body().text();73}7475/* Displays alert if this subscription overlaps another subscription for the same event. */76@Override77protected boolean overlaps(Subscription<?> other) {78if (other instanceof ToolUserAuthSubscription) {79ToolUserAuthSubscription that = (ToolUserAuthSubscription) other;8081/* The other subscription is considered to overlap if there is any82intersection in the chosen users and chosen actions. */83return (ObjectUtils.isBlank(this.users) || ObjectUtils.isBlank(that.users) || Collections.disjoint(84this.users,85that.users))86&& (ObjectUtils.isBlank(this.actions) || ObjectUtils.isBlank(that.actions)87|| Collections.disjoint(this.actions, that.actions));8889} else {90return false;91}92}93}
The following snippet declares a publisher. The user login-logout example restricts the receiver type to just ToolUser, and modifies the query to only allow users in the same role as the user who triggered the event to receive a notification. (This is arbitrary business logic but illustrates how the aforementioned methods can be overridden and customized.) This class also provides a preliminary filter on the baseline set of receivers.
1import com.psddev.cms.db.ToolRole;2import com.psddev.cms.db.ToolUser;3import com.psddev.cms.notification.Publisher;4import com.psddev.cms.notification.Receiver;5import com.psddev.dari.db.Query;67public class ToolUserAuthPublisher extends Publisher<ToolUserAuthSubscription, ToolUserAuthEvent> {89/* Constructor required by superclass to pass in the notification context. */10ToolUserAuthPublisher(ToolUserAuthEvent context) {11super(context);12}1314/* Narrows the types of receivers of this notification to ToolUsers. */15@Override16protected Class<? extends Receiver> getReceiverType() {17return ToolUser.class;18}1920/* Adds Query predicate to allow only users in the same role as that of the user21performing the action to receive notifications about that user. */22@Override23protected Query<? extends Receiver> getReceiversQuery() {2425ToolRole role = Optional.ofNullable(context.getUser())26.map(ToolUser::getRole)27.orElse(null);2829Query<? extends Receiver> query = super.getReceiversQuery();3031query.and("role = ?", role != null ? role : Query.MISSING_VALUE);3233return query;34}35}
The following snippet traps a login-logout event and starts the notification life cycle. The class extends Modification<ToolUserLoginToken> in order to modify the internal Brightspot object that keeps track of users logging in. The afterSave and afterDelete methods are overridden to serve as hooks for triggering the event. Saving a token implies a user logged in, and deleting a token implies a user logged out. Since the saves and deletes can happen in response to a user on the main UI event thread, the asynchronous version of the publish notification API is called.
1import com.psddev.cms.db.ToolUser;2import com.psddev.cms.db.ToolUserLoginToken;3import com.psddev.dari.db.Modification;4import com.psddev.dari.db.Query;56public class ToolUserLoginTokenModification extends Modification<ToolUserLoginToken> {78private transient boolean isNew;910/* An intermediate check to see if the token being saved is new (as opposed11to an existing one that's just being updated). This step is needed12because once the afterSave callback is triggered, the object will no13longer be considered "new." */14@Override15protected void beforeCommit() {16super.beforeCommit();17isNew = getState().isNew();18}1920/* After a new token is saved, find the user associated with the token and21publish a new ToolUserAuthEvent notification "asynchronously" with the22LOGIN action. */23@Override24protected void afterSave() {25super.afterSave();2627if (isNew) {28ToolUser user = getUser();2930if (user != null) {31new ToolUserAuthPublisher(new ToolUserAuthEvent(32user,33ToolUserAuthAction.LOGIN)).publishNotificationAsync();34}35}36}3738/* After a token is deleted, find the user associated with the token and39publish a new ToolUserAuthEvent notification "asynchronously" with the40LOGOUT action. */41@Override42protected void afterDelete() {43super.afterDelete();4445ToolUser user = getUser();4647if (user != null) {48new ToolUserAuthPublisher(new ToolUserAuthEvent(49user,50ToolUserAuthAction.LOGOUT)).publishNotificationAsync();51}52}5354/* Helper method to find the user associated with a token. */55private ToolUser getUser() {56return Query.from(ToolUser.class)57.where("_id = ?", getState().get("userId"))58.first();59}60}