Blogs
So in a recent project I've been building I reached a point where I believed my project would benefit from being able to issue user notifications.
For those that are not aware, Liferay has a built-in system for subscribing and notifications. Using these APIs, you can quickly add notifications to your projects.
Foundation
Before diving into the implementation, let's talk about the foundation of the subscription and notification APIs.
The first thing you need to identify is your events. Events are what users will subscribe to, and when events occur you want to issue notifications. Knowing your list of events going in will make things easier as you'll know when you'll need to send notifications.
For this blog post we're going to be implementing an example, so I'm going to build a system to issue a notification when a user with the Administrator role logs in. We all know that for effective site security no one should be using Administrator credentials because of the unlimited access Administrators have, so getting a notification when an Administrator logs in can be a good security test.
So we know the event, "Administrator has logged in", the rest of the blog will build out support for subscriptions and notifications.
One thing we will need to pull all of this together is a portlet module (since we have some interface requirements). Let's start by building a new portlet module. You can use your IDE, but to be IDE-neutral I'm going to stick with blade commands:
blade create -t mvcportlet -p com.dnebinger.admin.notification admin-notification
This will give us a new Liferay MVC portlet using our desired package and a simple project directory.
Subscribing To The Event
The first thing that users need to be able to do is to subscribe to your events. Some portal examples are blogs (where a user can subscribe to all blogs or an individual blog for changes). So the challenge for you is to identify where a user would subscribe to your event. In some cases you would display right on the page (i.e. blogs and forum threads), in other cases you might want to move it to a configuration panel.
For this portlet, there is no real UI work, just going to have a JSP with a checkbox for subscribe/unsubscribe, but the important part is the Liferay subscription APIs we're going to use.
Subscription is handled by the com.liferay.portal.kernel.service.SubscriptionLocalService service. When you're subscribing, you'll be using the addSubscription() method, and when you're unsubscribing you're going to use the deleteSubscription() method.
Arguments for the calls are:
- userId: The user who is subscribing/unsubscribing.
- groupId: (for adds) the group the user is subscribing to (for 'containers' like a doc lib folder or forum category).
- className: The name of the class user wants to be monitored of changes on.
- pkId: The primary key for the object to (un)subscribe to.
So normally you'll be building subscription into your SB entities, so it is common practice to put subscribe and unsubscribe methods into the service interface to combine the subscription features with the data access.
For this project, we don't really have an entity or a data access layer, so we're just going to handle subscribe/unsubscribe directly in our action command handler. Also we don't really have a PK id so we'll just stick with an id of 0 and the portlet class as the class name.
@Component(
immediate = true,
property = {
"javax.portlet.name=" + AdminNotificationPortletKeys.ADMIN_NOTIFICATION_PORTLET_KEY,
"mvc.command.name=/update_subscription"
},
service = MVCActionCommand.class
)
public class SubscribeMVCActionCommand extends BaseMVCActionCommand {
@Override
protected void doProcessAction(ActionRequest actionRequest, ActionResponse actionResponse) throws Exception {
String cmd = ParamUtil.getString(actionRequest, Constants.CMD);
if (Validator.isNull(cmd)) {
// an error
}
long userId = PortalUtil.getUserId(actionRequest);
if (Constants.SUBSCRIBE.equals(cmd)) {
_subscriptionLocalService.addSubscription(userId, 0, AdminNotificationPortlet.class.getName(), 0);
} else if (Constants.UNSUBSCRIBE.equals(cmd)) {
_subscriptionLocalService.deleteSubscription(userId, AdminNotificationPortlet.class.getName(), 0);
}
}
@Reference(unbind = "-")
protected void setSubscriptionLocalService(final SubscriptionLocalService subscriptionLocalService) {
_subscriptionLocalService = subscriptionLocalService;
}
private SubscriptionLocalService _subscriptionLocalService;
}
User Notification Preferences
Users can manage their notification preferences separately from portlets which issue notifications. When you go to the "My Account" area of the side bar, there's a "Notifications" option. When you click this link, you will normally see your list of notifications. But if you click on the dot menu in the upper right corner, you can choose "Configuration" to see all of the magic.
This page has a collapsable area for each registered notifying portlet and within each area is a line item for a type of notification and sliders to recieve notifications by email and/or website (assuming the portlet has indicated that it supports both types of notifications). In the future Liferay or your team might add more notification methods (i.e. SMS or other), and for those cases the portlet just needs to indicate that it also supports the notification type.
So how do we get our collapsable panel registered to appear on this page? Well, through the magic of OSGi DS service annotations, of course!
There are two types of classes that we need to implement. The first extends the com.liferay.portal.kernel.notifications.UserNotificationDefinition class. As the class name suggests, this class provides the definition of the type of notifications the portlet can send and the notification types it supports.
@Component(
immediate = true,
property = {"javax.portlet.name=" + AdminNotificationPortletKeys.ADMIN_NOTIFICATION},
service = UserNotificationDefinition.class
)
public class AdminLoginUserNotificationDefinition extends UserNotificationDefinition {
public AdminLoginUserNotificationDefinition() {
// pass in our portlet key, 0 for a class name id (don't care about it), the notification type (not really), and
// finally the resource bundle key for the message the user sees.
super(AdminNotificationPortletKeys.ADMIN_NOTIFICATION, 0,
AdminNotificationType.NOTIFICATION_TYPE_ADMINISTRATOR_LOGIN,
"receive-a-notification-when-an-admin-logs-in");
// add a notification type for each sort of notification that we want to support.
addUserNotificationDeliveryType(
new UserNotificationDeliveryType(
"email", UserNotificationDeliveryConstants.TYPE_EMAIL, true, true));
addUserNotificationDeliveryType(
new UserNotificationDeliveryType(
"website", UserNotificationDeliveryConstants.TYPE_WEBSITE, true, true));
}
}
This definition registers and informs notifications that we have a notification definition. The constructor binds the notification to our custom portlet and provides the message key for the notification panel. We also add two user notification delivery types that we'll support, one for sending an email and one for using the notification portlet.
When you go to the Notifications panel under My Account and choose the Configuration option from the menu in the dropdown on the upper-right corner, you can see the notification preferences:

Handling Notifications
Another aspect of notifications is the UserNotificationHandler implementation. The UserNotificationHandler's job is to interpret the notification event and determine whether to deliver the notification and build the UserNotificationFeedEntry (basically the notification message itself).
Liferay provides a number of base implementation classes that you can use to build your own UserNotificationHandler instance from:
- com.liferay.portal.kernel.notifications.BaseUserNotificationHandler - This implements a simple user notification handler with points to override the body of the notification and some other key points, but for the most part it is capable of building all of the basic notification details.
- com.liferay.portal.kernel.notifications.BaseModelUserNotificationHandler - This class is another base class suitable for asset-enabled entities for notifications. It uses the AssetRenderer for the entity class to render the asset and this is used as the message for the notifcation.
Obviously if you have an asset-enabled entity you're notifying on, you'd want to use the BaseModelUserNotificationHandler. For our implementation we're going to use BaseUserNotificationHandler as the base class:
@Component(
immediate = true,
property = {"javax.portlet.name=" + AdminNotificationPortletKeys.ADMIN_NOTIFICATION},
service = UserNotificationHandler.class
)
public class AdminLoginUserNotificationHandler extends BaseUserNotificationHandler {
/**
* AdminLoginUserNotificationHandler: Constructor class.
*/
public AdminLoginUserNotificationHandler() {
setPortletId(AdminNotificationPortletKeys.ADMIN_NOTIFICATION);
}
@Override
protected String getBody(UserNotificationEvent userNotificationEvent, ServiceContext serviceContext) throws Exception {
String username = LanguageUtil.get(serviceContext.getLocale(), _UKNOWN_USER_KEY);
// okay, we need to get the user for the event
User user = _userLocalService.fetchUser(userNotificationEvent.getUserId());
if (Validator.isNotNull(user)) {
// get the company the user belongs to.
Company company = _companyLocalService.fetchCompany(user.getCompanyId());
// based on the company auth type, find the user name to display.
// so we'll get screen name or email address or whatever they're using to log in.
if (Validator.isNotNull(company)) {
if (company.getAuthType().equals(CompanyConstants.AUTH_TYPE_EA)) {
username = user.getEmailAddress();
} else if (company.getAuthType().equals(CompanyConstants.AUTH_TYPE_SN)) {
username = user.getScreenName();
} else if (company.getAuthType().equals(CompanyConstants.AUTH_TYPE_ID)) {
username = String.valueOf(user.getUserId());
}
}
}
// we'll be stashing the client address in the payload of the event, so let's extract it here.
JSONObject jsonObject = JSONFactoryUtil.createJSONObject(
userNotificationEvent.getPayload());
String fromHost = jsonObject.getString(Constants.FROM_HOST);
// fetch our strings via the language bundle.
String title = LanguageUtil.get(serviceContext.getLocale(), _TITLE_KEY);
String body = LanguageUtil.format(serviceContext.getLocale(), _BODY_KEY, new Object[] {username, fromHost});
// build the html using our template.
String html = StringUtil.replace(_BODY_TEMPLATE, _BODY_REPLACEMENTS, new String[] {title, body});
return html;
}
@Reference(unbind = "-")
protected void setUserLocalService(final UserLocalService userLocalService) {
_userLocalService = userLocalService;
}
@Reference(unbind = "-")
protected void setCompanyLocalService(final CompanyLocalService companyLocalService) {
_companyLocalService = companyLocalService;
}
private UserLocalService _userLocalService;
private CompanyLocalService _companyLocalService;
private static final String _TITLE_KEY = "title.admin.login";
private static final String _BODY_KEY = "body.admin.login";
private static final String _UKNOWN_USER_KEY = "unknown.user";
private static final String _BODY_TEMPLATE = "<div class=\"title\">[$TITLE$]</div><div class=\"body\">[$BODY$]</div>";
private static final String[] _BODY_REPLACEMENTS = new String[] {"[$TITLE$]", "[$BODY$]"};
private static final Log _log = LogFactoryUtil.getLog(AdminLoginUserNotificationHandler.class);
}
This handler basically builds the body of the notification itself which will be based on the admin login details (user and where they are logging in from). When building out your own notification, you'll likely want to be using the notification event payload to pass details. We're going to pass just the host where the admin is coming from so our payload is as simple as it gets, but you could easily pass XML or JSON or whatever structured string you want to pass necessary notification details.
This handler just makes the same notification body for both email and notification portlet display, no difference between the two. Since the method is passed the UserNotificationEvent, you can use the getDeliveryType() method to build different bodies depending upon whether you are building an email notification or a notification portlet display message.
Publishing Notification Events
So far we have code to allow users to subscribe to our administrator login event, we allow them to choose how they want to receive the notifications, and we also have code to transform the notification event into a notification message, what remains is actually issuing the notification events themselves.
This is very much going to be dependent upon your event source. Most Liferay events are based on the addition or modification of some entity, so it is common to find their event publishing code in the service implementation classes when the entities are added or updated. Your own notification events can come from wherever the event originates, even outside of the service layer.
Our notification event is based on an administrator login; the best way to publish these kinds of events is through a post login component. We'll define the new component as such:
@Component(
immediate = true, property = {"key=login.events.post"},
service = LifecycleAction.class
)
public class AdminLoginNotificationEventSender implements LifecycleAction {
@Override
public void processLifecycleEvent(LifecycleEvent lifecycleEvent)
throws ActionException {
// get the request associated with the event
HttpServletRequest request = lifecycleEvent.getRequest();
// get the user associated with the event
User user = null;
try {
user = PortalUtil.getUser(request);
} catch (PortalException e) {
// failed to get the user, just ignore this
}
if (user == null) {
// failed to get a valid user, just return.
return;
}
// We have the user, but are they an admin?
PermissionChecker permissionChecker = null;
try {
permissionChecker = PermissionCheckerFactoryUtil.create(user);
} catch (Exception e) {
// ignore the exception
}
if (permissionChecker == null) {
// failed to get a permission checker
return;
}
// If the permission checker indicates the user is not omniadmin, nothing to report.
if (! permissionChecker.isOmniadmin()) {
return;
}
// this user is an administrator, need to issue the event
ServiceContext serviceContext = null;
try {
// create a service context for the call
serviceContext = ServiceContextFactory.getInstance(request);
// note that when you're behind an LB, the remote host may be the address
// for the LB instead of the remote client. In these cases the LB will often
// add a request header with a special key that holds the remote client host
// so you'd want to use that if it is available.
String fromHost = request.getRemoteHost();
// notify subscribers
notifySubscribers(user.getUserId(), fromHost, user.getCompanyId(), serviceContext);
} catch (PortalException e) {
// ignored
}
}
protected void notifySubscribers(long userId, String fromHost, long companyId, ServiceContext serviceContext)
throws PortalException {
// so all of this stuff should normally come from some kind of configuration.
// As this is just an example, we're using a lot of hard coded values and portal-ext.properties values.
String entryTitle = "Admin User Login";
String fromName = PropsUtil.get(Constants.EMAIL_FROM_NAME);
String fromAddress = GetterUtil.getString(PropsUtil.get(Constants.EMAIL_FROM_ADDRESS), PropsUtil.get(PropsKeys.ADMIN_EMAIL_FROM_ADDRESS));
LocalizedValuesMap subjectLocalizedValuesMap = new LocalizedValuesMap();
LocalizedValuesMap bodyLocalizedValuesMap = new LocalizedValuesMap();
subjectLocalizedValuesMap.put(Locale.ENGLISH, "Administrator Login");
bodyLocalizedValuesMap.put(Locale.ENGLISH, "Adminstrator has logged in.");
AdminLoginSubscriptionSender subscriptionSender =
new AdminLoginSubscriptionSender();
subscriptionSender.setFromHost(fromHost);
subscriptionSender.setClassPK(0);
subscriptionSender.setClassName(AdminNotificationPortlet.class.getName());
subscriptionSender.setCompanyId(companyId);
subscriptionSender.setCurrentUserId(userId);
subscriptionSender.setEntryTitle(entryTitle);
subscriptionSender.setFrom(fromAddress, fromName);
subscriptionSender.setHtmlFormat(true);
int notificationType = AdminNotificationType.NOTIFICATION_TYPE_ADMINISTRATOR_LOGIN;
subscriptionSender.setNotificationType(notificationType);
String portletId = PortletProviderUtil.getPortletId(AdminNotificationPortletKeys.ADMIN_NOTIFICATION, PortletProvider.Action.VIEW);
subscriptionSender.setPortletId(portletId);
subscriptionSender.setReplyToAddress(fromAddress);
subscriptionSender.setServiceContext(serviceContext);
subscriptionSender.addPersistedSubscribers(
AdminNotificationPortlet.class.getName(), 0);
subscriptionSender.flushNotificationsAsync();
}
}
This is a LifecycleAction component that registers as a post login lifecycle event listener. It goes through a series of checks to determine if the user is an administrator and, when they are, it issues a notification. The real fun happens in the notifySubscribers() method.
This method has a lot of initialization and setting of the subscriptionSender properties. This variable is of type AdminLoginSubscriptionSender, a class which extends SubscriptionSender. This is the guy that handles the actual notification sending.
The flushNotificationAsync() method pushes the instance onto the Liferay Message Bus where a message receiver gets the SubscriptionSender and invokes its flushNotification() method (you can call this method too if you don't need async notification sending).
The flushNotification() method does some permission checking, user verification, filtering (i.e. don't send a notification to the user that generated the event) and eventually sends the email notification and/or adds the user notification for the notifications portlet.
The AdminLoginSubscriptionSender class is pretty simple:
public class AdminLoginSubscriptionSender extends SubscriptionSender {
private static final long serialVersionUID = -7152698157653361441L;
protected void populateNotificationEventJSONObject(
JSONObject notificationEventJSONObject) {
super.populateNotificationEventJSONObject(notificationEventJSONObject);
notificationEventJSONObject.put(Constants.FROM_HOST, _fromHost);
}
@Override
protected boolean hasPermission(Subscription subscription, String className, long classPK, User user) throws Exception {
return true;
}
@Override
protected boolean hasPermission(Subscription subscription, User user) throws Exception {
return true;
}
@Override
protected void sendNotification(User user) throws Exception {
// remove the super classes filtering of not notifying user who is self.
// makes sense in most cases, but we want a notification of admin login so
// we know when never any admin logs in from anywhere at any time.
// will be a pain if we get notified because of our own login, but we want to
// know if some hacker gets our admin credentials and logs in and it's not really us.
sendEmailNotification(user);
sendUserNotification(user);
}
public void setFromHost(String fromHost) {
this._fromHost = fromHost;
}
private String _fromHost;
}
Putting It All Together
Okay, so now we have everything:
- We have code to allow users to subscribe to the admin login events.
- We have code to allow users to select how they receive notifications.
- We have code to transform the notification message in the database into HTML for display in the notifications portlet.
- We have code to send the notifications when an administrator logs in.
Now we can test it all out. After building and deploying the module, you're pretty much ready to go.
You'll have to log in and sign up to receive the notifications. Note if you're using your local test Liferay environment you might not have email enabled so be sure to use the web notifications. In fact, in my DXP environment I don't have email configured and I got a slew of exceptions from the Liferay email subsystem; I ignored them since they are from not having email set up.
Then log out and log in again as an administrator and you should see your notification pop up in the left sidebar.

Conclusion
So there you have it, basic code that will support creating and sending notifications. You can use this code to add notifications support into your own portlets and take them whereever you need.
You can find the code for this project up on github: https://github.com/dnebing/admin-notification
Enjoy!

