Liferay Custom Notifications

Java tutorial on how to handle Liferay custom notifications. There is very little good information on this topic but I’ve seen a good need for a proper documentation so here’s a nice example of how to handle Liferay User Notifications based on Liferay 6.2 Notifications API. The official documentation on Liferay notifications is rather weak but then again the feature is also rather new and it’s not even 100% part of the core Liferay experience. Liferay 6.2 notifications have improved quite well and I feel it’s time to make them a first-class feature.

If you download the Liferay 6.2 bundle with Tomcat then you have the Notifications portlet installed already. If you installed Liferay on your own Tomcat or other Application Server using the WAR installation method then you need to install Notifications Portlet from the Marketplace.
Here’s a link to the Notifications CE Portlet on Liferay Marketplace.

Liferay custom notifications require a User Notification Handler class that turns the notification into a nice HTML fragment that is displayed to the user. Also Liferay wants us to define the notification types that our portlet or application is creating in a definition file upfront.

So the first thing we need to do is to define the location of our class that extends BaseUserNotificationHandler class and definitions inside liferay-portlet.xml. This can be done in liferay-portlet.xml like this:


The important part is user-notification-definitions and user-notification-handler-class tags. Now we need to actually define our notification inside that example-user-notification-definitions.xml. This file should go into your resources folder. Since I’m using a maven based project then this file goes into src/main/resources/:

location of example-user-notification-definitions.xml

If you are running an Ant based project then you have to place this file inside docroot/WEB-INF/src/ folder.

Here’s the example-user-notification-definitions.xml I created:

<?xml version="1.0"?>
<!DOCTYPE user-notification-definitions PUBLIC "-//Liferay//DTD User Notification Definitions 6.2.0//EN" "">

Notice how we are using some public final Strings from class files like this: ${com.example.notifications.ExampleUserNotificationHandler.PORTLET_ID}. This allows us to define our Portlet ID in one location and keep things a little bit simpler.
Ok, so now we have our example defined and we can actually Add a new notification for the user. We can do so by using the built-in UserNotificationEventLocalServiceUtil in either our Service Builder class or one of our portlet classes.

Here’s how to add a new notification event:

    JSONObject payloadJSON = JSONFactoryUtil.createJSONObject();
    payloadJSON.put("userId", user.getUserId());
    payloadJSON.put("yourCustomEntityId", exampleEntity.getEntityId());
    payloadJSON.put("additionalData", "Your notification was added!");

		(new Date()).getTime(),
		false, serviceContext);		

If you are doing this inside a portlet then you need the serviceContext, you can get one like this:

ServiceContext serviceContext = ServiceContextFactory.getInstance(portletRequest);

So now that we have our notification inside the database we want to actually show it to the user. When we look at the dockbar our notification number gets increased so we can be pretty sure that the notification was added. When you attempt to actually look at the notification you will probably see a blank white box. We need the notification handler class to actually handle the display side of our notification. This current example also handles the behavior like providing Approve and Reject buttons right in our example notification so that our example user could perform an action straight from the dockbar notification and not waste extra time when it’s a trivial task. I don’t have the correct screenshot but the look we are going after is similar to that you can see on this picture:
liferay custom notifications

Basic idea being we have an Approve and Reject buttons on our notification, so that you can click on the notification or it’s background to go and View more details, but you can also directly perform some custom actions like Approve or Reject in this Liferay custom notification tutorial. The buttons could also be “Report as Spam” or “Notify the Security”. It’s all up to you where that link takes the user.
So here’s our handler class:

import javax.portlet.ActionRequest;
import javax.portlet.PortletURL;
import javax.portlet.WindowState;

import com.liferay.portal.kernel.json.JSONFactoryUtil;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.notifications.BaseUserNotificationHandler;
import com.liferay.portal.kernel.portlet.LiferayPortletResponse;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.model.UserNotificationEvent;
import com.liferay.portal.service.ServiceContext;
import com.liferay.portal.service.UserNotificationEventLocalServiceUtil;

public class ExampleUserNotificationHandler extends
		BaseUserNotificationHandler {
	public static final String PORTLET_ID = "example_WAR_exampleportlet";
	public ExampleUserNotificationHandler() {



	protected String getBody(UserNotificationEvent userNotificationEvent,
			ServiceContext serviceContext) throws Exception {

		JSONObject jsonObject = JSONFactoryUtil

		long yourCustomEntityId = jsonObject

		String title = "<strong>Example notification for entity ID "
				+ yourCustomEntityId
				+ "</strong>";

		String bodyText = "Some other text.";

		LiferayPortletResponse liferayPortletResponse = serviceContext

		PortletURL confirmURL = liferayPortletResponse.createActionURL(com.example.notifications.ExampleUserNotificationHandler.PORTLET_ID);

		confirmURL.setParameter(ActionRequest.ACTION_NAME, "doSomethingGood");
		confirmURL.setParameter("redirect", serviceContext.getLayoutFullURL());
		confirmURL.setParameter("yourCustomEntityId", String.valueOf(yourCustomEntityId));
		confirmURL.setParameter("userNotificationEventId", String.valueOf(userNotificationEvent.getUserNotificationEventId()));

		PortletURL ignoreURL = liferayPortletResponse.createActionURL(com.example.notifications.ExampleUserNotificationHandler.PORTLET_ID);
		ignoreURL.setParameter(ActionRequest.ACTION_NAME, "cancelForExample");
		ignoreURL.setParameter("redirect", serviceContext.getLayoutFullURL());
		ignoreURL.setParameter("yourCustomEntityId", String.valueOf(yourCustomEntityId));
		ignoreURL.setParameter("userNotificationEventId", String.valueOf(userNotificationEvent.getUserNotificationEventId()));

		String body = StringUtil.replace(getBodyTemplate(), new String[] {
				"[$CONFIRM$]", "[$CONFIRM_URL$]", "[$IGNORE$]",
				"[$IGNORE_URL$]", "[$TITLE$]", "[$BODY_TEXT$]" }, new String[] {
				serviceContext.translate("approve"), confirmURL.toString(),
				serviceContext.translate("reject"), ignoreURL.toString(),
				title, bodyText });
		return body;

	protected String getLink(UserNotificationEvent userNotificationEvent,
			ServiceContext serviceContext) throws Exception {

		JSONObject jsonObject = JSONFactoryUtil

		long yourCustomEntityId = jsonObject
		LiferayPortletResponse liferayPortletResponse = serviceContext

		PortletURL viewURL = liferayPortletResponse.createActionURL(com.example.notifications.ExampleUserNotificationHandler.PORTLET_ID);
		viewURL.setParameter(ActionRequest.ACTION_NAME, "showDetails");
		viewURL.setParameter("redirect", serviceContext.getLayoutFullURL());
		viewURL.setParameter("yourCustomEntityId", String.valueOf(yourCustomEntityId));
		viewURL.setParameter("userNotificationEventId", String.valueOf(userNotificationEvent.getUserNotificationEventId()));
		return viewURL.toString();

	protected String getBodyTemplate() throws Exception {
		StringBundler sb = new StringBundler(5);
		sb.append("<div class=\"title\">[$TITLE$]</div><div ");
		sb.append("class=\"body\">[$BODY_TEXT$]<a class=\"btn btn-action ");
		sb.append("btn-success\" href=\"[$CONFIRM_URL$]\">[$CONFIRM$]</a>");
		sb.append("<a class=\"btn btn-action btn-warning\" href=\"");
		return sb.toString();


NB Important Information: The com.example.notifications.ExampleUserNotificationHandler.PORTLET_ID string that you use as your notification type has to match an actual portlet ID. It doesn’t actually need to be YOUR portlet ID but that would be the right thing to have there. The reason being that Notifications display portlet uses it to display a small portlet icon next to your notification to help the user identify the source of the notification. Providing a bad Portlet ID or something like null leads to a hard-to-trace NullPointerException in the JSP. Took me an hour to track it down.

The problematic part is this code from notifications-portlet view_entries.jsp source

<span class="portlet-icon">
<liferay-portlet:icon-portlet portlet="<%= PortletLocalServiceUtil.getPortletById(company.getCompanyId(), userNotificationEvent.getType()) %>" />

If you have a question or having trouble getting it working then feel free to write me a comment and I’ll do my best to help you out. If you would like to read more about Liferay related subjects then also make sure to comment on that and if you liked what you read then make sure to subscribe – It’s FREE!

Liferay portlet file upload tutorial

Here’s a Liferay portlet file upload tutorial to show you how to handle file uploading by clients.
First we need to define our upload form JSP. This can be shown using Liferay MVCPortlet’s default view.jsp.
We will define our upload form as an AlloyUI form element and w


<%@ taglib uri="" prefix="portlet" %>
<%@ taglib uri="" prefix="aui"%>
<%@ taglib uri="" prefix="liferay-ui"%>
<portlet:defineObjects />

<portlet:actionURL name="upload" var="uploadFileURL"></portlet:actionURL>

<aui:form action="<%= uploadFileURL %>" enctype="multipart/form-data" method="post">

	<aui:input type="file" name="fileupload" />
	<aui:button name="Save" value="Save" type="submit" />


And now we need to handle the upload action that we are calling by submiting this form.
Note some important parts that people often miss in their code, like checking if there’s enough room on the device to actually store the file and refuse additional file uploads when the device has less than 1GB of free space.

Full example class


import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;


import com.liferay.portal.kernel.upload.UploadPortletRequest;
import com.liferay.portal.util.PortalUtil;
import com.liferay.util.bridges.mvc.MVCPortlet;

public class RelatedFilesPortlet extends MVCPortlet {

	private final static int ONE_GB = 1073741824;
	private final static String baseDir = "/tmp/uploaded/";
	private final static String fileInputName = "fileupload";

	public void upload(ActionRequest request, ActionResponse response)
			throws Exception {

		UploadPortletRequest uploadRequest = PortalUtil.getUploadPortletRequest(request);

		long sizeInBytes = uploadRequest.getSize(fileInputName);

		if (uploadRequest.getSize(fileInputName) == 0) {
			throw new Exception("Received file is 0 bytes!");

		// Get the uploaded file as a file.
		File uploadedFile = uploadRequest.getFile(fileInputName);

		String sourceFileName = uploadRequest.getFileName(fileInputName);

		// Where should we store this file?
		File folder = new File(baseDir);

		// Check minimum 1GB storage space to save new files...
		if (folder.getUsableSpace() < ONE_GB) {
			throw new Exception("Out of disk space!");

		// This is our final file path.
		File filePath = new File(folder.getAbsolutePath() + File.separator + sourceFileName);

		// Move the existing temporary file to new location.
		FileUtils.copyFile(uploadedFile, filePath);


Java tutorial: Add Portlet to Liferay Control Panel

You can add portlet to liferay control panel. For your custom control panel portlet you have to modify liferay-portlet.xml file and add 2 lines there:


The control-panel-entry-category has to be one of “my”, “content”, “portal” or “server” and specifies under which category should the portlet be displayed.
The weight specifies the ordering or the elements under the different categories. It’s a double number, and the higher you specify the lower your portlet is placed. So if you want to appear in the bottom, add “99.0”.
The portlet has to be non-instanceable (the Control Panel may have only such portlets) so you also have to have this line:


Please note that Control Panel by default does not allow you to enter CONFIG mode of the portlet. For a work-around you can use


Full example liferay-portlet.xml


add portlet to liferay control panel

Service Builder String column length in Liferay

Service Builder generated entity String columns have a standard length of 75 characters. Here’s a tutorial on how to change the Service Builder String column length.
If you are looking at the most obvious place like service.xml file then you are wrong. You have to look into portlet-model-hints.xml file.
Now this file is updated when you run service builder, but it will keep your modifications to column entries.


<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.2.0//EN" "">

<service-builder package-path="com.example" auto-namespace-tables="false">
	<entity name="Entity" local-service="true" human-name="Entity" table="my_entity" remote-service="true">

		<column name="entityId" type="long" primary="true" />

		<column name="groupId" type="long" />
		<column name="companyId" type="long" />
		<column name="userId" type="long" />
		<column name="userName" type="String" />
		<column name="createDate" type="Date" />
		<column name="modifiedDate" type="Date" />

		<column name="veryLongField" type="String"></column>
		<column name="approvalDate" type="Date"></column>
		<column name="comments" type="String"></column>



Now assuming we have an Entity with fields like veryLongField and comments and approvalDate we can modify the auto-generated portlet-model-hints.xml file to look something like this:

<?xml version="1.0"?>
	<hint-collection name="TEXTAREA">
		<hint name="display-height">100</hint>
		<hint name="display-width">500</hint>
		<hint name="max-length">32768</hint>
	<hint-collection name="DATEFIELD">
		<hint name="month-nullable">true</hint>
		<hint name="day-nullable">true</hint>
		<hint name="year-nullable">true</hint>
		<hint name="show-time">false</hint>
	<model name="com.example.Entity">
		<field name="entityId" type="long" />
		<field name="groupId" type="long" />
		<field name="companyId" type="long" />
		<field name="userId" type="long" />
		<field name="userName" type="String" />
		<field name="createDate" type="Date" />
		<field name="modifiedDate" type="Date" />
		<field name="veryLongField" type="String">
			<hint name="max-length">255</hint>
		<field name="approvalDate" type="Date">
			<hint-collection name="DATEFIELD" />
			<hint name="year-range-delta">90</hint>
			<hint name="year-range-future">true</hint>
		<field name="comments" type="String">
			<hint-collection name="TEXTAREA" />

Notice how we are defining a hint for max-legth so that the database column would also be 255 characters.
We are also defining a hint-collection to re-use the same hints multiple times.
Once we have run service builder again, the corresponding SQL table files and entity classes are updated to reflect our changes.
In case you are wondering, yes there are few more hints that you can use.

Model Hint Values and Descriptions

Name Value Type Description Default
auto-escape boolean sets whether text values should be escaped via HtmlUtil.escape true
autoSize boolean displays the field in a for scrollable text area false
day-nullable boolean allows the day to be null in a date field false
default-value String sets the default value for a field (empty String)
display-height integer sets the display height of the form field rendered using the aui taglib 15
display-width integer sets the display width of the form field rendered using the aui taglib 350
editor boolean sets whether to provide an editor for the input false
max-length integer sets the maximum column size for SQL file generation 75
month-nullable boolean allows the month to be null in a date field false
secret boolean sets whether hide the characters input by the user false
show-time boolean sets whether to show include time along with the date true
upper-case boolean converts all characters to upper case false
year-nullable boolean allows the year to be null in a date field false
year-range-delta integer specifies the number of years to display from today’s date in a date field rendered with the aui taglib 5
year-range-future boolean sets whether to include future dates true
year-range-past boolean sets whether to include past dates true
Here be dragons
default-day-delta integer ??? ???
default-month-delta integer ??? ???
default-year-delta integer ??? ???
check-tab boolean ??? ???

For the full list of Model Hints for Liferay 6.2 you can take a look at this page in the developer guide:

As you can see it’s actually rather simple to modify Service Builder String column length.

If you have a question or having trouble getting this thing working then feel free to write me a comment and I’ll do my best to help you out. If you would like to read more about Liferay related subjects then also make sure to comment on that and if you liked what you read then make sure to subscribe – It’s FREE!

Why use Liferay?

The Good side of Liferay

Liferay is NOT just a Java based CMS. It’s a complete portal environment that’s suitable for large enterprise usage. It should be used as a platform to develop in-house applications or intranet. If you need just a small website to promote something then Liferay is not for you.

Some people compare Liferay with PHP based CMS systems like Joomla or Wordpress but that’s completely wrong. Liferay does contain CMS and it’s not a bad one. It’s rather useful for providing small applications and even better for big applications.

Supporting JSR 168 and JSR 286 means you can deploy Portlet applications that others have developed. Liferay also has a very good support for groups and communities allowing you to build team sites and custom environments for different user-groups with ease. Some cool features that you get with Liferay out of the box are:

  • User and Group management
  • Multi-site support for having different pages and themes for different user groups
  • Proper Access rights and Role management
  • Internationalization
  • Document library management
  • Wiki, Blogging and Forums
  • Service Oriented Architecture
  • Theme support
  • Integrates with other existing systems using LDAP, RSS, iFrames and other technologies



The Bad side of Liferay

Developing applications and portals for the first time is a taunting task and comes with a rather massive learning curve. If you plan to use Liferay Service Builder and do everything “the Liferay way” then you have to plan probably at least couple of weeks for learning it first.

If you need to develop a small application quickly AND you don’t have an existing Liferay installation already in-house then you can get your application done way faster by NOT using Liferay. But once you have learned Liferay and have enough need for proper portal then Liferay becomes rather irresistible.

When it comes to documentation and tutorials then it’s not the best situation either. It does have a massive developer’s guide. But the Wiki is filled with tons of content that’s rather old and not updated for the latest releases.

The official developer and administration guides are good but often lack information about some crucial parts or fail to provide decent examples. While they claim to be developer friendly I have to admit that some parts of the development platform is severely lacking. Also note that releases are rather infrequent. Also one should be very cautious because early versions are always rather buggy.

AlloyUI that’s provided with Liferay for Javascript and UI is rather difficult to learn and seems also a very large investment time-wise – save yourself the trouble and use jQuery or some other favorite JS library instead.

Download PDF or JPG from portlet

To create a Download PDF link or serve images in your portlet, you need to implement serveResource method in your Liferay portlet class. Here’s an example on how to serve a JPG file from the server.

First we need to define a link to our file, this can be done using portlet taglib resourceURL like this.

<portlet:resourceURL var="niceImageUrl">
	<portlet:param name="fileName" value="flowers.jpg" />
<img src="<%= niceImageUrl %>" alt="flowers.jpg" />

Please note that you need to have portlet taglib in your JSP file header.

<%@ taglib uri="" prefix="portlet" %>

This generates a resource URL, so now we must implement the serveResource method to handle this call.

public class ImagesPortlet extends MVCPortlet {

	public void serveResource(ResourceRequest request, ResourceResponse response)
			throws IOException, PortletException {

		String fileName = ParamUtil.getString(request, "fileName");

		File outputFile = new File("/Folder/On/Server/"+fileName);

		OutputStream out = response.getPortletOutputStream();
		InputStream in = new FileInputStream(outputFile);
		IOUtils.copy(in, out);

For PDF files you could easily tune it to have a link to your PDF like this:

<portlet:resourceURL var="pdfDownloadUrl">
	<portlet:param name="fileName" value="document.pdf" />
<a href="<%= pdfDownloadUrl %>">Download PDF</a>

And in the portlet class you can use code like this

public class PDFPortlet extends MVCPortlet {

	public void serveResource(ResourceRequest request, ResourceResponse response)
			throws IOException, PortletException {

		String fileName = ParamUtil.getString(request, "fileName");

		File outputFile = new File("/Folder/On/Server/"+fileName);

		OutputStream out = response.getPortletOutputStream();
		InputStream in = new FileInputStream(outputFile);
		IOUtils.copy(in, out);

It’s important to use application/pdf content type so that the browser would know how to properly handle the file.

If you need to output multiple file types or handle different actions then you can pass an extra parameter that decides what file type to serve. Another idea could be to use a parameter to decide between serving a file or serving a JSON response, in case you need need to retrieve some data using AJAX. Here’s a simple example on how to pass content type in the URL to use it during serving the file.

<portlet:resourceURL var="pdfDownloadUrl">
	<portlet:param name="type" value="application/pdf" />
	<portlet:param name="fileName" value="document.pdf" />
<a href="<%= pdfDownloadUrl %>">Download PDF</a>

<portlet:resourceURL var="niceImageUrl">
	<portlet:param name="type" value="image/jpg />
	<portlet:param name="fileName" value="flowers.jpg" />
<img src="<%= niceImageUrl %>" alt="flowers.jpg" />

And in the portlet class you can just output the provided content type without having to detect it during download time. This allows you to define the expected content types dynamically, which can be very useful when you are generating a file list.

public class FilesPortlet extends MVCPortlet {

	public void serveResource(ResourceRequest request, ResourceResponse response)
			throws IOException, PortletException {

		String type = ParamUtil.getString(request, "type");
		String fileName = ParamUtil.getString(request, "fileName");

		File outputFile = new File("/Folder/On/Server/"+fileName);

		response.setContentType( type );
		OutputStream out = response.getPortletOutputStream();
		InputStream in = new FileInputStream(outputFile);
		IOUtils.copy(in, out);

More info on how to generate PDF on the fly can be read in this useful blog post.

As always, free feel to ask questions and provide suggestions in the comments.

AUI Autocomplete with service builder json datasource

Since it’s not so trivial to correctly understand AlloyUI Autocomplete that’s shipped with Liferay 6.2 then here’s an example on how to make an autocomplete that uses a custom service builder json web service as a datasource.
We will use AUI Autocomplete widget and then user Liferay Service library to fetch the result from our own custom service builder built JSON web service.

This is the look we are trying to achive:
aui autocomplete service builder

Full portlet view.jsp code:

<%@ taglib uri="" prefix="portlet" %>
<%@ taglib uri="" prefix="liferay-theme" %>
<%@ taglib uri="" prefix="liferay-ui"%>
<%@ taglib uri="" prefix="aui" %>

<liferay-theme:defineObjects />

<aui:input name="contactName" type="text" />

<aui:script use="autocomplete-list,aui-base,aui-io-request-deprecated,autocomplete-filters,autocomplete-highlighters,datasource,datasource-get,datatable-datasource">

// Please note that this contact portlet service is a service builder generated JSON web service.
// We pass the groupId as a query param because our service expects it. Liferay has a nice javascript method for finding the group id.
var contactSearchDS = new A.DataSource.IO({source: '/api/jsonws/'+Liferay.ThemeDisplay.getScopeGroupId()});

var contactSearchQueryTemplate = function(query) {
        // Here's an example on how to pass additional parameters to the query for you service
        // In our case we are fetching only the first 20 items and specify the ordering by name
	var output = '&name='+query.trim()+'&sort=name&dir=asc&start=0&end=20';
	return output;

var contactSearchLocator = function (response) {
	var responseData = A.JSON.parse(response[0].responseText);
// For debugging you can do: console.debug(responseData);
    return responseData;

var contactSearchFormatter = function (query, results) {
	return, function (result) {
// For debugging: console.debug(result.raw);
		return '<strong>'+result.raw.fullName+'</strong><br/>'' '' ';

var contactSearchTextLocator = function (result) {
// This is what we place in the input once the user selects an item from the autocomplete list.
// In our case we want to put contact full name in there.
	return result.row.fullName;

var contactSearchInput = new A.AutoCompleteList({
	allowBrowserAutocomplete: 'false',
	resultHighlighter: 'phraseMatch',
	activateFirstItem: 'false',
	inputNode: '#<portlet:namespace/>contactName',
	render: 'true',
	source: contactSearchDS,
	requestTemplate: contactSearchQueryTemplate,
	resultListLocator: contactSearchLocator,
        resultFormatter: contactSearchFormatter,
	resultTextLocator: contactSearchTextLocator		


We’ll post an example of “multi-autocomplete” input also soon. That one requires a lot more code but allows selecting multiple contacts and shows them as “tags” above the input box. Much like the facebook name finder works.

As always, if you have questions or suggestions then please comment below.

How to create a taglib with JSP’s for Liferay

This is a bit tricky since we want to be able to nicely use our tag library in all our multiple Portlet projects, so the taglib should be a maven dependency. This usually means a Jar file. This is all nice and we can easily fit our Tag classes in the jar. We can also easily fit our TLD file in META-INF folder and it’s nicely available. Now comes the tricky part – What if we want our tag output to be a separate JSP page. Like almost all the Liferay tags are. JSP allows us to access all Liferay theme variables, other taglibs and all other useful things while generating our tag output.

The problem is that you can’t load the JSP from a jar file easily so the JSP’s have to be actually outside of your reusable jar project. So Liferay has an IncludeTag class that we want to extend that provides us with JSP output.

So we end up having 2 projects:

  • One for the Taglib TLD file and actual Tag classes that’s turned into JAR that we can use as a dependency in our other portlet projects.
  • One for the JSP files that is a Hook so the JSP’s are always available.

Here’s the example Tag class, this goes into our “example-userlist-taglib” project.
Notice that we have a JSP path that follows the Liferay convention on naming the files.

import javax.servlet.http.HttpServletRequest;
import com.liferay.taglib.util.IncludeTag;

public class UserList extends IncludeTag {

	private static final boolean _CLEAN_UP_SET_ATTRIBUTES = true;

	private static final String _PAGE = "/html/taglib/example/userlist/page.jsp";

	private String label;

	protected String getPage() {
		return _PAGE;

	public int doStartTag() {

	protected void cleanUp() {

	protected boolean isCleanUpSetAttributes() {

	protected void setAttributes(HttpServletRequest request) {
		request.setAttribute("example:user-list:label", getLabel());

	public String getLabel() {
		return label;

	public void setLabel(String label) {
		this.label = label;


Now an example TLD file on how to define a tag in taglib that should be in /src/main/resources/META-INF/ if you are running a maven based project:

<?xml version="1.0"?>
<taglib version="2.0" xmlns=""

Solution to our JSP loading problem is actually rather simple: Create a Hook project that contains all your taglib JSP’s and deploy that on any Liferay server you need to use your taglib. The files for my userlist tag have to go into META-INF/custom_jsps/html/taglib/example/userlist/ folder for example.

I actually have an init.jsp and a page.jsp file there, examples follow:

<%@ taglib uri="" prefix="portlet"%>
<%@ taglib uri="" prefix="aui"%>
<%@ taglib uri="" prefix="liferay-portlet"%>
<%@ taglib uri="" prefix="liferay-theme"%>
<%@ taglib uri="" prefix="liferay-ui"%>
<%@ taglib uri="" prefix="liferay-util"%>
<%@ page import="javax.portlet.PortletRequest"%>
<%@ page import="javax.portlet.PortletResponse"%>
<%@ page import="javax.portlet.PortletPreferences"%>
<portlet:defineObjects />
<liferay-theme:defineObjects />
PortletRequest portletRequest = (PortletRequest)request.getAttribute(JavaConstants.JAVAX_PORTLET_REQUEST);
PortletResponse portletResponse = (PortletResponse)request.getAttribute(JavaConstants.JAVAX_PORTLET_RESPONSE);

String label = (String)request.getAttribute("example:user-list:label");


and now the userlist tag jsp:

<%@ include file="init.jsp"%>
<div class="userlist">
	<strong><%=label %></strong>:
	<!-- Add code here for listing your users. -->

So there you have it! Now we can easily use our tag in any portlet jsp, provided we first include our taglib like this:

<%@ taglib uri="" prefix="example" %>

and then we can do:

<example:user-list label="Admin users" />