User-specific versus shared portlet preferences - part 2

This article is the second and last in a series of 2 posts on how personalization can be achieved in a portal project. In this post I'll explain how to combine user-specific and shared portlet preferences in one portlet. This will allow portlets to have a common, shared set of preferences and additionally a set of user-defined preferences on top of those shared preferences.

See also User-specific versus shared portlet preferences - part 1.

Recap: preferences scopes

In the first part, the standard portlet personalization concepts were explained with regard to portlet preferences. We learned that preferences can be configured to be stored in a certain scope. Such scope enables preferences to be shared across users and/or portlet instances.

Choosing the right scope depends on the use case that the portlet tries to fulfill. If portlet preferences should only be modified by an administrative role, it makes sense to share this configuration throughout the site that the portlet lives in (or maybe even throughout the portal). If individual users should be able to modify the behavior, content or visualization of the portlet, it makes more sense to store preferences on a per-user basis. In this case, each user sees his or her own version of the portlet.

Problem situation

By configuring a combination of properties in liferay-portlet.xml we can easily change the preferences storage scope. However, this scope applies to allpreferences. By default, there is no way to store some preferences in one scope and some preferences in another. This is nevertheless a valid use case.

Consider the QuickLinks portlet of the first part of this post series again. If we configure the per-user quick links, shared across portlets setting we can allow each user to define his or her own quick links. But maybe the portlet always needs to have a set of general quick links, defined by a site administrator, that should not be modified nor deleted by normal users. Normal users are only allowed to define additional quick links on top of these common links.

In this example, we clearly need a solution that requires preferences to be stored in multiple scopes at the same time, i.e. a user-specific scope and a more general, site-level scope. We cannot achieve this by configuration alone. Instead, we'll have to reside to custom implementation.

Our multi-scope preferences API

We wrote a custom, reusable implementation layer on top of the preferences service layer of Liferay and called this the ScopedPreferencesService.This interface has different methods, corresponding to the different scope configurations we identified in the previous post.

  • getPortalWidePreferences(PortletRequest)
  • getSiteWidePreferences(PortletRequest)
  • getPortletSpecificPreferences(PortletRequest)
  • getUserSpecificPreferences(PortletRequest)
  • getUserSpecificPortletSpecificPreferences(PortletRequest)

Every method returns an actual javax.portlet.PortletPreferences object. So we can interact with this object as if it was coming directly from the PortletRequest. The difference is that, when you call the store method on the object, Liferay will not look at liferay-portlet.xml to know in which scope to save the preferences. This is now the responsibility of our custom service. Explaining in detail how this is done, will lead us to far. Check out the Github code if you're interested in the internal logic.

Example

In this example we'll write a portlet that is able to display the content of an RSS feed. Every user can configure a personal RSS feed. If a user does not have configured an RSS feed by himself, a general RSS feed will be used that is configurable by the site administrator.

In our portlet class, we have a doView method that first tries to retrieve the user specific RSS feed. If this one is not defined, the site scoped RSS feed will be used. Apart from that, we have two processAction methods: one for storing the per-user RSS feed (configurable by every site member) and one for storing the site-wide RSS feed (configurable only by the site administrator).

QuickLinksBean.java
public class QuickLinksPortlet extends MVCPortlet {
 
     private ScopedPreferencesService prefService = new ScopedPreferencesServiceImpl();
 
     @Override
     public void doView(RenderRequest request, RenderResponse response) {
         String feedUrl = prefService.getUserSpecificPreferences(request).getValue( "feedUrl" , "" );
         if (feedUrl.isEmpty()) {
             feedUrl = prefService.getSiteWidePreferences(request).getValue( "feedUrl" , "" );
         }
         renderRequest.setAttribute( "feedUrl" , feedUrl);
         super .doView(request, response);
     }
 
     @ProcessAction (name= "storeUserPrefs" )
     public void storeUserPrefs(ActionRequest request, ActionResponse response) {
         String feedUrl = request.getParameter( "feedUrl" );
         PortletPreferences prefs = prefService.getUserSpecificPreferences(request);
         prefs.setValue( "feedUrl" , feedUrl);
         prefs.store();
     }
 
     @ProcessAction (name= "storeSitePrefs" )
     public void storeSitePrefs(ActionRequest request, ActionResponse response) {
         String feedUrl = request.getParameter( "feedUrl" );
 
         PortletPreferences prefs = prefService.getSiteWidePreferences(request);
         prefs.setValue( "feedUrl" , feedUrl);
         prefs.store();
     }
 
}

Prerequisites

Your portlet must be non-instanceable if you want to use the multi-scope preferences API. This means that you need to set the instanceable parameter to false in liferay-portlet.xml. As this is the default value, you could also just omit the parameter altogether.

If you use the ScopedPreferencesService, it's no longer important what preference scope related parameters are set in liferay-portlet.xml. Instead, you'll need to decide for yourself what preferences are stored in what scope by calling the right method from the ScopedPreferencesService.

Conclusion

By creating a layer on top of the preferences API of Liferay we are now able to build portlets that store their preferences in multiple scopes at the same time. This further enhances the personalization aspect of Liferay, one of the key features of the platform.

Check out or clone the code in Github: https://github.com/limburgie/scoped-preferences-service.

If you like this solution or, even better, have some suggestions for improvements, please drop a comment or spread the word via Twitter (@pmesotten).

Blogs
Tried with Liferay 6.1.2, I got stuck on getting NullPointerException. After replacing "LIFERAY_SHARED_THEME_DISPLAY" with WebKeys.THEME_DISPLAY on line 76 in the Impl class, everthing works fine yet.
Thanks for experimenting! Indeed, the web key for ThemeDisplay was changed between 6.1 and 6.2, so it is better to use the static variable. I don't know why I didn't do that in the first place.
Another fix emoticon

It is possible that I am not doing everything well, but here's the problem: if I save the preferences, it is saved with id 86 and not with id of my portlet. Long story short - here is modified body of getPreferences:

long companyId = portal.getCompanyId(portletRequest);
String portletId = (String) portletRequest.getAttribute(WebKeys.PORTLET_ID);
String portletResource = ParamUtil.getString(portletRequest, "portletResource");
if (Validator.isNotNull(portletResource)) {
portletId = portletResource;
}
return portletPreferencesService.getPreferences(companyId, ownerId, ownerType, plid, portletId);
Hi Peter,

The code was only tested for Liferay 6.2, so it may be possible that the behavior is different in 6.1. If I found some time I'll do some 6.1 testing as well and take your patch into consideration. Thanks again!
This is a great blog. I would like to know if there are any performance considerations . If we use the preferences will there be any performance benefits or side effects? . I am looking for a user specific (or user scoped) preferences . Would it be better to have a servicebuilder create a table and have these user specific preferences managed or is it better to use the way this article explained? please let me know.