Liferay 5.2.3: custom URLs formats

Liferay SEO capabilities seems to be surprisingly weak when it comes to URL management. Consider an example: you’re trying to build a webapp that will be doing some abstract searches over some search data sources, and present the results on one page.

You want page to have URL like http://<host>%5B:<port>%5D/section/subsection/search/<keyword>%5B?someParam=<value>%5D
Particular goals: URL can be generated by other website that knows nothing of our Liferay-based portal internals, and it (URL) should be nice and bookmarkable.

On the page you want to have some portlets, provided by different development teams/vendors, that would get the keyword and present results. The portlets should be independent since new ones can be added over time, and you want to be able to order development of several new portlets in parallel via several independent vendors. Thus every portlet on page should be able to obtain <keyword> and <value> passed in URL to page.

So, what does Liferay provide?
1. URL patterns are /web/<organization>/<page> by default. There are a couple of tricks to handle “/web/<organization>” part (configuring virtual hosts etc), yet they’re not very intuitive.
2. Friendly URL mapper (Liferay 6 and Liferay 5 differ here BTW) that at best will allow you to have URLs like /web/guest/search/-/searchportlet_A/keyword/<keyword> – if your portlet with ID “searchportlet_A” is non-instanceable, this and only this particular portlet will get “keyword” parameter value from this URL.

I didn’t spend too much time digging in details of these solutions (and thus it might be I’m wrong about some details), but it’s obvious we’re not going to get far with this. So what can we do?

It turns out that Liferay has Tucky URL Rewriter filter in it, disabled by default. Using this filter we could map URLs that we want to a page that we want (doing it with forward of-course, no redirects from nice URL to ugly URLs or anything like this).
It will be a task for our portlet(s) to render proper custom URLs, but that’s not a big problem, it’s just nothing will facilitate us to do it. But well, this is the price of having absolutely custom format of URLs. Thus we can get our page by any URL we want.

But next question is – how to obtain outer URL and it’s parameters from portlets on a page?
Digging in Liferay 5 API revealed the somewhat obscure com.liferay.portal.util.PortalUtil class that provides static methods like getCurrentURL(PortletRequest) and PortalUtil.getHttpServletRequest(PortletRequest) + PortalUtil.getOriginalHttpServletRequest(ServletRequest).

Seems like we’ve got what we need, right? Not yet.
PortalUtil methods work Ok… until you enable forwarding via Tucky URL Rewrite filter. Once you enable it, URLs like “/c/layout/” will be returned instead of expected original URL that user typed into his browser’s address bar.
Obviously, author of the code didn’t expect that original request could already be a forwarded request. Well, not good – you did included Tucky URL Rewriter, didn’t you, so why using it is should come as something unexpected? (-;

Fortunately, sources of all the stuff are available, so I could quickly identify a problem and make a fix. Cheers to open-source!
We want our solution to be deployable to any Liferay 5 instance customers might already have, thus I didn’t put fixed into Liferay sources, but make a small “wrapper” for PortalUtil instead.

package com.mykola.makhin.liferay.portal.util;

import javax.portlet.PortletRequest;
import javax.servlet.http.HttpServletRequest;

import com.liferay.portal.util.PortalUtil;

public class LiferayUtil {

	public static String getOuterRequestUrl(PortletRequest portletRequest) {
		String result;

		HttpServletRequest servletRequest = PortalUtil.getHttpServletRequest(portletRequest);
		String forwardedRequestUri = (String) servletRequest.getAttribute("javax.servlet.forward.request_uri");
		String forwardedRequestQueryString = (String) servletRequest.getAttribute("javax.servlet.forward.query_string");
		if (forwardedRequestUri != null) {
			result = forwardedRequestUri;
			if (forwardedRequestQueryString != null) {
				result += "?" + forwardedRequestQueryString;
			}
		} else {
			result = PortalUtil.getCurrentURL(portletRequest);
		}

		return result;
	}
	
	public static String getOuterRequestParameter(PortletRequest portletRequest, String paramName) {
		HttpServletRequest servletRequest = PortalUtil.getHttpServletRequest(portletRequest);
		
		return PortalUtil.getOriginalServletRequest(servletRequest).getParameter(paramName);
	}
}

Ok, so we can obtain external URL and URL parameters with this utility, how do we put it all together?
Here’s the plan:
1. Enable and configure Tucky URL rewriter.
How to do this? We use Tomcat-bundled distribution of Liferay 5, so direct way would be to edit web.xml and urlrewrite.xml in “tomcat-6.0.18/webapps/ROOT/WEB-INF” folder, but more smart would be to deploy it using Liferay EXT environment.
In EXT go to “ext-web/docroot/WEB-INF”, edit web.xml and create+edit urlrewrite.xml files (note: don’t append -ext to file names).
Contents of web.xml will be merged with other contents that are in Liferay already, so what we put in web.xml file in EXT is just this:

<?xml version="1.0"?>                                                                                                                                                                                                                                           
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
        <filter>
                <filter-name>URL Rewrite Filter</filter-name>
                <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
                <init-param>
                        <param-name>logLevel</param-name>
                        <param-value>ERROR</param-value>
                </init-param>
                <init-param>
                        <param-name>statusEnabled</param-name>
                        <param-value>true</param-value>
                </init-param> 
        </filter>
        <filter-mapping>
                <filter-name>URL Rewrite Filter</filter-name>
                <url-pattern>/*</url-pattern>
        </filter-mapping>
</web-app>

Contents of urlrewrite.xml for our example will be something like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 2.6//EN" "http://tuckey.org/res/dtds/urlrewrite2.6.dtd">

<urlrewrite>
	<rule>
		<from>/section/subsection/search/([^\?]+)\?(.*)</from>
		<to type="forward">/web/myorganization/search?keyword=$1&amp;$2</to>
	</rule>	
	<rule>
		<from>/section/subsection/search/([^\?]+)$</from>
		<to type="forward">/web/myorganization/search?keyword=$1</to>
	</rule>	
</urlrewrite>

2. Now we can use LiferayUtil.getOuterRequestParameter(PortletRequest, “keyword”) and LiferayUtil.getOuterRequestParameter(PortletRequest, “anyParameter”) in our portlet to obtain parameters we need (note that in original URL “keyword” is not a parameter, but in rewritten it is, and thus getting it from forwarded request works just allright).

3. The only this left to us is to render URLs with proper format in our Portlets.
Since we can put any parameter in rewritten url (i.e. make something like <to type=”forward”>/web/myorganization/search?urlRewritten=true&amp;keyword=$1</to>) we can be aware whether portlet is on page accessed by nice SEO friendly URL or not, and correspondingly render either custom format SEO friendly URLs, or just do usual portlet render/action URLs.

That’s it.
I want to point out that resulting solution is quite easy to deploy to production environments that already have Liferay running.
Though EXT environment it a bit heavy, we can always ship only our two files. They can be written to clean distribution of EXT, downloaded straight from official site, and then ext can be deployed the usual way. And next time on same environment we don’t even need to do this again of course, we’ll only have to deploy our new/updated portlets.

Conclustion: I’ve spent a couple of days (partially, not the entire days he-he (-:) thinking and digging until I came up with 100% working solution (and relatively easily deployable too). I’m a bit surprised I could not google-up a solution like this, and especially surprised with what’s offered officially from Liferay (because rest of my experience with Liferay was rather positive so far). And curse Portlet APIs (d-:) for not even thinking about things like this.

Hopefully this post will save somebody time and effort of figuring out how to accomplish this task. After all, having a custom URL format for your webapp must be simple in year 2010, right? (-;
And once again, open source approach (as well as usage of standard tools – Tucky URL Rewriter in this case) saved time and effort, and actually made some things possible that otherwise would not be (or seem to be) so.

Advertisements

2 thoughts on “Liferay 5.2.3: custom URLs formats

  1. Thanks for the tip man. This is exactly what I needed. I love how cumbersome java web development is. I laugh at it most of the time, it’s how I get through it.

    • You’re welcome, Aaron.
      Yes, J2EE is Java Evil Edition ((-: , and everything that’s somehow related to it (or any “Enterprise”) is bears the Evil mark (((-:

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