« Back

User-specific versus shared portlet preferences - part 2

Technical Blogs August 10, 2014 By Peter Mesotten

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).

Threaded Replies Author Date
Both parts of this article are very nice.... Vipin Bardia December 30, 2014 1:54 AM
Tried with Liferay 6.1.2, I got stuck on... Peter Drábik January 26, 2015 4:03 AM
Thanks for experimenting! Indeed, the web key... Peter Mesotten January 26, 2015 4:22 AM
Another fix :D It is possible that I am not... Peter Drábik January 26, 2015 11:01 PM
Hi Peter, The code was only tested for Liferay... Peter Mesotten January 26, 2015 11:18 PM
[...] String fontSize = “14px”; int ownerType =... Anonymous March 26, 2015 3:25 AM
[...] Per recuperare una delle due preferences,... Anonymous March 26, 2015 4:30 AM
[...] Per recuperare una delle due preferences,... Anonymous March 26, 2015 4:35 AM
Very useful :) Barmanand kumar April 11, 2016 2:10 AM

Both parts of this article are very nice.
Thanks emoticon
Posted on 12/30/14 1:54 AM.
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.
Posted on 1/26/15 4:03 AM.
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.
Posted on 1/26/15 4:22 AM in reply to Peter Drábik.
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);
Posted on 1/26/15 11:01 PM.
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!
Posted on 1/26/15 11:18 PM in reply to Peter Drábik.
[...] String fontSize = “14px”; int ownerType = ResourceConstants.SCOPE_INDIVIDUAL; long ownerId = portal.getUserId(portletRequest); long plid = themeDisplay(portletRequest).getPlid(); long companyId =... [...] Read More
Posted on 3/26/15 3:25 AM.
[...] Per recuperare una delle due preferences, per esempio la grandezza del font che l’utente ha scelto, basterà usare le stesse variabili per recuperare il PortletPreferences e usare il metodo: CODICE... [...] Read More
Posted on 3/26/15 4:30 AM.
[...] Per recuperare una delle due preferences, per esempio la grandezza del font che l’utente ha scelto, basterà usare le stesse variabili per recuperare il PortletPreferences e usare il metodo: ... [...] Read More
Posted on 3/26/15 4:35 AM.
Posted on 4/11/16 2:10 AM.