Liferay (6) practices

Working with Liferay always raises a lot of questions about best practices, or even just correct or even working ways to do things. That’s because Liferay is quite non-standard in many ways (which isn’t surprising actually, as really all Portlet containers invent their own tricks due to limitations of Portlets specificatin).

Today I’ll share some generic practices which I wish I myself knew when I was starting my Liferay experience.


Embedding portlets and webcontent in theme – re-use webcontents and portlets instances on multiple pages
I have a hundred pages in my site, I want to have shared header+footer+navigation present on all those pages. Pretty usual thing to ask, isn’t it? And quite important at the same time.
So how can I do this in Liferay?

It doesn’t have “Books” like BEA Portal has, where you can put pages in one “Book”, and they all ‘ll inherit content from the book. It doesn’t seem to have any similar functionality. So, were stuck? Ready to throw Liferay out to a trash bin? Don’t rush. Though this is indeed a serious shortcoming (actually it must be the most notable one that I’ve encountered in Liferay), there is a remedy to that.

The answer is custom theme with webcontent items or portlets “embedded” in theme itself. It’s not very flexible solution, but it does the job.

Preliminary things – put those to portal-ext.properties:

journal.template.velocity.restricted.variables=
journal.article.force.autogenerate.id=false

First one will allow access to some things in Theme velocity templates.
Second one will allow you to create webcontent items with custom (non-autogenerated) IDs, so you could use IDs like “GENERICHEADER” instead of autogenerated numbers (like 12355 or whatever).

So, to the point.
In our custom theme _diffs/templates/portal_normal.vm we define this macro:

#macro(showArticleContent $article_id)
$!journalContentUtil.getContent($getterUtil.getLong("$!group_id"), "$!article_id", null, "$!locale", $theme_display)
#end

And now wherever in this template we want to render some webcontent items, we just add something like:

#showArticleContent("GENERICHEADER")

If hardcoded ID isn’t what you’re really looking for, or for some reason you’re not allowed to disable autogenerated IDs for webcontent, you can do it with Custom Fields (Control Panel: Portal: Custom Fields: Page: Edit). Let’s say we added custom field for pages, with same code – “GENERICHEADER”.
So we put value like 12355 to (default) value of page custom field, and now we just have to modify our macro accordingly:

#macro(showArticleContent $fieldCode)
#set($article_id = $!theme_display.getLayout().getExpandoBridge().getAttribute($fieldCode))
$!journalContentUtil.getContent($getterUtil.getLong("$!group_id"), "$!article_id", null, "$!locale", $theme_display)
#end

For a particular details look up JournalContentUtil class.

And embedding portlets in a theme is actually already described on Liferay website

By the way, it’s reasonable to put any reusable stuff into portal_normal.vm. For example, we use some JavaScript code to count statistics of page accesses. The same JS code has to be present on absolutely all pages, so we just put it in the theme’s portal_normal.vm – that simple.

Link Portlet URLs to another page + Configuration action
Liferay has an interesting feature that allows you to make portlet URLs point to other Portal page than the one on which the portlet was rendered. It’s nice to use when you want for example list/details pages for something – you put List portlet on List page, it lists for example company employees. You want employee Details page to open when user clicks on some of employees in a list – how would you do that?

Pretty simple – in “Look and Feel” portlet configuration (a bit weird place for it but whatever) you have “Link portlet URLs to Page” setting, where you can choose the page. Then, if your List portlet, say, renders action URLs, and on action it sends IPC event to details portlet with employee ID, you can now place those two portlets on two different pages – it’s still work. User will click action URL, action will be sent to your List portlet, IPC event from it will be sent to Details portlet, and Details page with Details portlet will be opened. That’s pretty nice.

However, there is one significant downside – if you have an EDIT mode in your portlet, where you display some form with configurations, you’ll see that it submits… to that other page too. And all in all EDIT mode doesn’t look too nice in Liferay.

To remedy that, you can implement Configuration Action for your portlet, and thus have your preferences edited in “Configuration” popup instead of separate EDIT mode.

Liferay website actually has quite detailed description of way to add/implement configuration action – so you probably should look it up there.
One thing to note though is that Portlet Preferences of your portlet aren’t available in Configuration Action in a usual way – instead you have to make a call PortletPreferencesFactoryUtil.getPortletSetup(actionRequest, ParamUtil.getString(actionRequest, “portletResource”)) to obtain them, so don’t miss this.

In-memory time-based caching using Liferay Web Cache Util
That’s a nice facility in Liferay to allow you to cache stuff in memory for a specified period of time.

Let’s say that for example you build a portlet that displays content obtained from some REST webservice by certain URL. We want to cache the response of that webservie. Cache key will be the URL.
Let’s say the API is webServiceClientInstance.getResponse(URL url).

In our portlet we have code like

WebServiceResponse response = webServiceClientInstance.getResponse(someUrl);
request.setAttribute("wsResponse", response);

Let’s create WSCacheItem class that implements com.liferay.portal.kernel.webcache.WebCacheItem:

public class WSCacheItem implements WebCacheItem {
private static final long serialVersionUID = -123123123123123123L; // Generated UID
private WebServiceClient webServiceClientInstance;
private URL url;

public WSCacheItem(WebServiceClient webServiceClientInstance, URL url) {
this.webServiceClientInstance = webServiceClientInstance;
this.url = url;
}

@Override
public WSResponse convert(String key) throws WebCacheException {
try {
WSResponse response = webServiceClientInstance.getResponse(url);
return response;
} catch (Exception e) {
throw new WebCacheException(e);
}
}

@Override
public long getRefreshTime() {
return REFRESH_TIME;
}

private static final long REFRESH_TIME = Time.HOUR * 2; // Cached for 2 hours
}

Now our portlet code changes to this:

WebCacheItem wci = new WSCacheItem(webServiceClientInstance, url);
WSResponse response;
try {
response = (WSResponse)WebCachePoolUtil.get(OurPortlet.class.getName() + StringPool.PERIOD + url.toExternalForm(), wci);
} catch(ClassCastException e) {
WebCachePoolUtil.clear();
response = (ShopResponse)WebCachePoolUtil.get(OurPortlet.class.getName() + StringPool.PERIOD + url.toExternalForm(), wci);
}
request.setAttribute("wsResponse", response);

The “OurPortlet.class.getName() + StringPool.PERIOD + url.toExternalForm()” part is just to have unique per URL cache key for our portlet.

As for ClassCastException, this little trick is for hot redeploys of portlet, when cached item class is unloaded and new one is loaded – we have to clear cache and re-cache our item.

As you can see, the code isn’t very complex, but the logic for web-service call moves to WebCacheItem implementation convert method.

Custom fields and problem with Document Library documents (DLFileEntry)
This is just a smaller hint, but might save some hours of digging for you.

As you may know, Liferay allows you to define custom fields for different types of Liferay objects, like for Organizations for instance, or for Document Library documents.

If you define custom field “myfield” on Organization, and you call:
organizationInstance.getExpandoBridge().getAttribute(“random”) – returns null, because there’s no field “random” defined;
organizationInstance.getExpandoBridge().getAttribute(“myfield”) – returns value as expected.
Kind of obvious. But… DLFileEntry (document library documents) is very special here.

dlFileEntry.getExpandoBridge().getAttribute(“random”) – returns null, which is expected because there is no custom field “random”.
dlFileEntry.getExpandoBridge().getAttribute(“myfield”) – always returns default value! (like empty string for Text, 1 Jan 1970 for Date etc), which is extremely confusing.

So, how do we get the actual value of custom field for Document Library Document? Well, this way:
dlFileEntry.getFileVersion().getExpandoBridge().getAttribute(“myfield”) – returns desired value.

About these ads

12 Responses to Liferay (6) practices

  1. Hi there, calling $organizationInstance.getExpandoBridge().getAttribute(“myfield”) from my VM template does not work? I have tried $organization and $company also but to no avail – any clues would be very helpful – Jamie

    • mvmn says:

      You don’t have $organizationInstance variable set by default.
      If you didn’t do #set($organizationInstance = ) then it won’t have any organization in it.
      In my post I just used it as an example meaning that in some code you have an instance of Organization object, and it’s in variable named, say, “organizationInstance”. Or “instanceOfOrganization”. Or whatever.

      Speaking of your case, is this a webcontent template? $company should contain a current company AFAIK, but it’s not the same as Organization.
      I’ll check it all a bit later.

      • mvmn says:

        > If you didn’t do #set($organizationInstance = )
        If you didn’t do #set($organizationInstance = [obtain organization instance somehow])

    • mvmn says:

      I’ve checked, and $company does contain instance of Company.
      However Company isn’t the same as Organization. I know this sounds confusing, but that’s how it is.
      If you have heard of Instances, Company in Liferay is actually same as Instance.

      What are you trying to achieve actually?

  2. [...] Wie man z.B. Footer global überall verfügbar macht und gleichzeitig die Inhalte als WebContent pflegen kann: http://mvmn.wordpress.com/2011/06/20/liferay-practices-6-0-6/ [...]

  3. Shantanu says:

    I get, ‘Type mismatch: cannot convert from Serializable to String’ with
    dlFileEntry.getFileVersion().getExpandoBridge().getAttribute(“myfield”) ;

    How to resolve this?

    • mvmn says:

      dlFileEntry.getFileVersion().getExpandoBridge().getAttribute(“myfield”).toString()

      • Shantanu says:

        Thanks for prompt reply. But that results in Ljava.lang.String;@1ce5c9c and not actual String.
        BTW, The custom field is select list with three values of which atleast one is set to each document.

        • mvmn says:

          Well, you have array of Strings there obviously.

          Object myfieldVals = dlFileEntry.getFileVersion().getExpandoBridge().getAttribute(“myfield”);

          if(myfieldVals instanceof String[]) {
          for(String theVal : ((String[])myfieldVals) ) {
          // Do domething with theVal, like this:
          System.out.println(theVal);
          }
          }

          This is basic Java, nothing specific to Liferay.

  4. Shantanu says:

    Well, you caught me. I was actually searching Liferay API to solve this.
    After reading your blog, my experience is the same as you rightly mentioned…. “This is just a smaller hint, but might save some hours of digging for you”.
    Thanks a lot.

  5. amit says:

    Hi ,will you tell me that how we set values for custom fields in document library.
    I want to add a custom field in DL documents and wants to that values come from another table dynamically in dro down.
    Please help me in which class we have to do coding for that.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: