Portal Hook Plugins

Lately, Liferay has been following a trend toward externalizing extensibility features as much as possible. The traditional EXT model, while powerful, often proves too complex for many situations, or too intrusive to the product to retrain a high level of maintainability and a comfortable upgrade path because by it's very nature it inadvertently promotes bad programming practices which can easily introduce difficult migration issues.

There are still use cases for using the EXT extension model (some less component friendly app servers for example), but the plan is to minimize the need and outright eliminate it wherever possible.

That in mind, the external extensibility features of Liferay have been very un-trendily dubbed "plugins". Liferay supports 5 different types of plugins out-of-the-box. They are:

  • Portlets
  • Themes
  • Layout Templates
  • Webs
  • Hooks

Today I'd like to focus on the newest addition, "Hooks". Hooks have been Brian Chan's own personal pet project for the last several months. As the name implies they allow "hooking" into Liferay. Specifically they allow you to hook into the eventing system, model listeners, jsps and portal properties. We'll begin by creating a bare hook config file where we will define some hooks as we review the different types

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 5.1.0//EN" "http://www.liferay.com/dtd/liferay-hook_5_1_0.dtd">

<hook>
</hook>

Event Handlers

Liferay has a few event handler connection points throughout its lifecycle, designed to allow developers to conveniently hook-in custom logic. The available events are:

  • Application Startup Events (application.startup.events)
  • Login Events (login.events.pre, login.events.post)
  • Service Events (servlet.service.events.pre, servlet.service.events.post)

Generally speaking your event implementations should extend com.liferay.portal.kernel.events.Action.

For example, presuming we have a custom event handler which should fire when the portal is starting to process any request called me.auge.ray.ServicePreAction, this class placed in the plugin work dir of <sdk_root>/hooks/rays-hook/docroot/WEB-INF/src/me/auge/ray/ServicePreAction.java, I would place the following element in the config file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 5.1.0//EN" "http://www.liferay.com/dtd/liferay-hook_5_1_0.dtd">

<hook>
	<event>
<event-class>me.auge.ray.ServicePreAction</event-class>
<event-type>servlet.service.events.pre</event-type>
</event>
</hook>

A couple points to make about events are that an application.startup.events can only extend com.liferay.portal.kernel.events.SimpleAction as that one expects an array of companyIds for which it should be invoked. The second is that you can define as many implementations as you like for each event type. Simply repeat the event element for each one.

Model Listeners

Model listeners are similar in behavior to portal event handlers except that these handle events with respect to models built using ServiceBuilder. These listeners implement the com.liferay.portal.model.ModelListener interface.

If you wanted to listen for new Blog posts, you might have a class called me.auge.ray.NewBlogEntryListener which looked like this:

package me.auge.ray;
import com.liferay.portal.ModelListenerException;
import com.liferay.portal.model.BaseModel;
import com.liferay.portal.model.ModelListener;
import com.liferay.portlet.blogs.model.BlogsEntry;
public class NewBlogEntryListener implements ModelListener {
	public void onAfterCreate(BaseModel arg0) throws ModelListenerException {
BlogsEntry entry = (BlogsEntry)arg0;
System.out.println("Woohoo! We got an new one called: " + entry.getTitle());
}
public void onAfterRemove(BaseModel arg0) throws ModelListenerException { } public void onAfterUpdate(BaseModel arg0) throws ModelListenerException { } public void onBeforeCreate(BaseModel arg0) throws ModelListenerException { } public void onBeforeRemove(BaseModel arg0) throws ModelListenerException { } public void onBeforeUpdate(BaseModel arg0) throws ModelListenerException { } }

You'd configure it like so:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 5.1.0//EN" "http://www.liferay.com/dtd/liferay-hook_5_1_0.dtd">

<hook>
	<event>
		<event-class>me.auge.ray.ServicePreAction</event-class>
		<event-type>servlet.service.events.pre</event-type>
	</event>
	<model-listener>
<model-listener-class>me.auge.ray.NewBlogEntryListener</model-listener-class>
<model-name>com.liferay.portlet.blogs.model.BlogsEntry</model-name>
</model-listener>
</hook>

As with events, add as many as you like even per model.

JSPs

One of the biggest aspects of implementing the portal is of course customization of the user experience. This largely involves modifying portal jsps. The problem is of course migration from version to version where you may end up with code squew, code management, version management, and many other issues. JSP hooks are designed to aleviate some of those issues by providing a way for SI's to easily modify jsps without having to alter the core. Simply specify a folder in the hook plugin from which to obtain jsp files and the portal will automatically use those in place of existing ones in the portal. This works for any jsps in the portal, portlets, servlets, and tags. All you need to do is make sure that you follow the same folder structure off your specified folder.

For example if you specify the folder /WEB-INF/jsps, the changing the view for the blogs portlet would require a file in /WEB-INF/jsps/html/portlet/blogs/view.jsp. Configuration would look like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 5.1.0//EN" "http://www.liferay.com/dtd/liferay-hook_5_1_0.dtd">

<hook>
	<event>
		<event-class>me.auge.ray.ServicePreAction</event-class>
		<event-type>servlet.service.events.pre</event-type>
	</event>
	<model-listener>
		<model-listener-class>me.auge.ray.NewBlogEntryListener</model-listener-class>
		<model-name>com.liferay.portlet.blogs.model.BlogsEntry</model-name>
	</model-listener>
	<custom-jsp-dir>/WEB-INF/jsps</custom-jsp-dir>
</hook>

Portal Properties

We can alter the portal's configuration properties by specifying an override file. The properties in this file will immediately take effect when deployed thus allowing runtime re-configuration of the portal.

If you had a file /WEB-INF/src/portal.properties, the configuration would look like:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 5.1.0//EN" "http://www.liferay.com/dtd/liferay-hook_5_1_0.dtd">

<hook>
	<event>
		<event-class>me.auge.ray.ServicePreAction</event-class>
		<event-type>servlet.service.events.pre</event-type>
	</event>
	<model-listener>
		<model-listener-class>me.auge.ray.NewBlogEntryListener</model-listener-class>
		<model-name>com.liferay.portlet.blogs.model.BlogsEntry</model-name>
	</model-listener>
	<portal-properties>portal.properties</portal-properties>
	<custom-jsp-dir>/WEB-INF/jsps</custom-jsp-dir>
</hook>

Finally, all of the above hooks will immediately revert their targetted functionality as soon as they are undeployed from the portal. Also, each type of hook can easily be disabled via portal.properties (Note that if properties hook is disabled, a hook cannot be used to re-enable it). Hooks can be built, packaged, and deployed, like other plugins, using the Liferay plugins SDK.

Blogs
With this new hooks I can drop all customization done through the ext environment except some spring (extensions in ext-spring.xml) and some customizations in the PortalImpl class.

Really looking forward to migrate the current customizations to hooks.
Very nice! That really makes customization easier. One question a colleague of mine asked today about this very thing is: Can you defined your custom AuthenticatorS with hooks or is that still something you need to deploy to the core portal?
ok, so suppose you want to only update the login.jsp page, what do you do exactly?
Very helpful. A while ago I added new tables using EXT env. where the hibernate mapping files, java codes are generated using built-service task. Can I achieve this using Hook? If so how? Is it also possible to override Struts mapping file?

Thank you so much.
You can use ServiceBuilder from the plugins SDK, you don't need hooks, although you can combine that and hooks to achieve different types of integrations. See the example portlets in the SVN repository, plugins repository portlets, or wiki for how to use ServiceBuilder in plugins SDK.

Overridding struts mapping files is not currently something you can do using Hooks.
Simply place the jsp file for the login page in 'custom-jsp-dir'. Make sure it's the identical path from the top of the context.
Ray, Maybe I'm mis-reading the blog but I was hoping to create a liferay-hook.xml file in WEB-INF pointing to a custom jsp directory under WEB-INF. I was expecting liferay to use the alternate path to find the relevant jsp page and if that jsp doesn't exist in the alternate path, to then look under the default path. However in reading these posts, I'm now wondering if the only way to utilize jsp hooks is to use the SDK and ant scripts (which will simply replace the jsps in the default path with my custom jsps).
Thank you Ray for a very helpful note.
Anyway, is there anyway to access request object within ModelListener?
Sorry, no. What exactly do you need from the request? There might be other ways of getting it.
I also need the request. In the method onAfterCreate, i create an user in Alfresco with web services. But if it happens something wrong in that creation, i need to post an error (with SessionErrors, i suppose) to the create_account.jsp.

Thanks a lot for your help
This is great!
Some reflections/questions:

I tried the following: add a portal.properties with auth.pipeline.pre=my.Autenticator,com.liferay.portal.security.auth.LDAPAuth
and added the my.Authenticator to my portlet. I get a ClassNotFoundException thogh. I guess its because LR use another class loader than my portlet.

Is it possible to add an own Autenticator through liferay-hook.xml?

Another thing, when I added login.events.pre/login.events.post and redeployed the portlet using hot-deploy (copy into $user/liferay/deploy). The previous registred actions still fires (now un deployed) still fired as well as the new one....
Q1 - Sorry, no Authenticators as of right now, but definitely a top candidate for inclusion.

Q2 - Yup, this is a known bug, fixed in trunk. Will be in a the next 5.1.x release.
I have to create a custom autologin mechanism and was wondering if it was possible to develop it as a hook.
Not as of yet, but it shouldn't be too hard to implement the hook-autologin feature. It's essentially the same pattern as with Events.

Would you be willing to take a crack at it? We're really strapped for dev cycles right now.
Where do I put the hook config file?
This is not clear to me from this description
I guess your confusion is like mine, originally... I looked at https://lportal.svn.sourceforge.net/svnroot/lportal/plugins/trunk/hooks/ to figure it out...

There is no create.sh in the hooks folder like other plugins.

Seems to me that:

1- you create a folder in hooks, with an arbitrary name
2- your hooks config goes in yourFolder/docroot/WEB-INF/liferay-hook.xml
3- your overrides go in /docroot/WEB-INF/src and need to be referenced by your hook config
4- you should have a yourFolder/docroot/WEB-INF/liferay-plugin-package.properties
5- you probably want to add a yourFolder/build.xml for ant
You can actually place your hook descriptor and any dependencies inside any other plugin WAR you already have.

So, unless you specifically want to separation (not a bad idea in some cases) it's not explicitly required.
It is really a great move!

I am trying to understand the functionality how hook is working, I want to modify the view.jsp of sms portlet can any body instruct me how to go about this.
Hi,
From my LoginPostAction defined in liferay-hook.xml I'm trying to use the session attribute "com.liferay.portal.servlet.SharedSessionAttributeCache" with no success.

I can get the object, using session.getAttribute("com.liferay.portal.servlet.SharedSessionAttributeCache"); but when I try to cast it to a 'usable' type, com.liferay.portal.servlet.SharedSessionAttributeCache, I either get an class not found exception, or a class cast exception (com.liferay.portal.servlet.SharedSessionAttributeCache cannot be cast to com.liferay.portal.servlet.SharedSessionAttributeCache)

The problem (I guess) is that my hook is lodad in the webbapps classloader and my webbapps classloder dont have access to the SharedSessionAttrributeCache class from portal-impl.jar (loaded by ROOT-webbapp). To use the same classloader using
<Context>
<Loader loaderClass="com.liferay.support.tomcat.loader.PortalClassLoader"/>
</Context>
is not an option (different versions of some libraries in webbapp than in LR). I have tried including the portal-impl.jar file as well as just putting a version of class file in web-inf/classes/ with no success. How can I form my LoginPostAction access the sharedSessionAttributesCache which is pressent in the httpSession i can get from the request parameter. Is this a "bug" or am I missing something?

Best regards
Erik
Hi Erik, hi Ray,

i've run into the same issue i guess. Currently i'm using Plugins SDK 5.23 and tried to write a hook plugin for the event auth.pipeline.pre. The hook is hot-deployed without errors but when trying to sign in got class not found exception:

07:45:55,943 ERROR [InstancePool:107] Unable to load com.ext.portal.security.auth.PanelAuth with the portal class loader or the current context class loader
java.lang.ClassNotFoundException: com.ext.portal.security.auth.PanelAuth at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1387) at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1233)

I've tried with and without setting the portal class loader by /META-INF/context.xml but got the same error.

Have you guys got some idea how to workaround this issue, maybe i have to keep using EXT for implement this event handler instead Plugins SDK?

Thanks
Laszlo
I have the same problem.

Any solution/answer to this yet? I need to be able to make a check against another database before the user is authenticated against the liferay db. With the pre auth pipeline I get the classnotfoundexception and with the pre login the user is already authenticated before the action happens (I don't understand the point of the pre login if it's like this).

Thankful for answers.
Ray, This is great! Is there a complete working sample implementation of each of the hook handlers?
Great, I am looking for sample implementation for message board ( add new message and new category )
like BlogEntry class , Do we have MessageBoardEntry or CategoryEntry etc.
I'm not sure I understand the question here. Can you elaborate?

Edit: Nevermind I get it. I'd have to test that scenario!
Ray Auge,

I am trying to implement hook for blogs. Trying to create custom Action class and invoke custom Java classes. Is there a sample or a pointer on how we achieve this. ?

I tried just having the code under web-inf/src but the hook is not picking it ip. Any help is appreciated.

Thanks
LR09
Ray,

I'm having troubles in fully understand how Event Handlers work. Could you provide a brief but intuitive example of a possible application for Event Handlers and show some sample code of how you would roughly implement a custom Action class in that case?

Your help is much appreciated!
My clients have asked me to keep a counter for every portlet that represents the number of Views for that portlet. I will need to create another portlet that displays a summary. I think hooks can be helpful. Please help me. How can I do this? Somehow I need my class member to get executed every time a portlet is displayed and increase counter for that portlet.
We need <b>locales</b> (not mentioned in http://svn.liferay.com/browse/portal/trunk/definitions/liferay-hook_5_2_0.dtd?r=trunk) to override in hook portal.properties<br />how can we achieve it
Quote: "Hooks can be built, packaged, and deployed, like other plugins, using the Liferay plugins SDK."

But how?
Portlets only need a create.sh/create.bat or similar, and a lot of easy and well documented step by steps instruction.

I tried to make a language hook to overwrite some language files without any success. Tried making it by hand and couldn't even build the hook, and tried to deploy it as a new portlet making the war, deploying it but seeing no results at all.

A fool-proof example and an easy to run shell script would be a great addition.
Hi Ray

Thanks for the write up on hooks. I need some clarifiaction on this

Im relatively new to Liferay and have been assigned this huge task of trying to integrate it with and ecommerce product to implement some concepts of social shopping. We have identified some areas like Tagging, groups, forums etc that we are looking to extend. For Eg. we arelooking at a functionality where a user on an ecommerce site would be able to tag products.
What we are trying to figure out is how to go about extending Tagging in liferay. Do we have to use EXT for this? or is there a way to achieve this using hooks?

I would really appreciate it if you could give me some pointers on this.

Thanks in anticipation
Have created a very simple jsp-hook portlet, that does not seem to override the jsp. Can anybody please look at the war file and tell me what I am doing wrong?
TIA
Ray for a layman like me can you explain if hooks would be a good suggestion in this case?

I need to modify the iframe portlet with hard coded url and username data for say 20 sites, and also maintain the original iframe portlet.

Is it possible to just copy the iframe portlet code, modify it and then deploy it as a portlet or should a hook be used? I really dont get the hook concept.

thanks
Did you ever figure this out? I'm trying to do nearly the same thing! The question is, how do you create a new portlet, by extending the built-ins? I've not seen much via google.
What activities actually trigger login.events.pre and login.events.post events?

I have created some very simple handlers for these events. I see my handlers get called when I login with a valid username/password. I assumed that a login.events.pre event would be fired during an attempted (but unsuccessful) login -- eg. bad password, but this is not happening.
So, it seems that in 5.2 at least the hook configuration in liferay-hook.xml was changed completely. No events anymore, just <portal-properties>, if I got it right from reading the DTD. Is there any updated hook tutorial available for 5.2?
Please clarify. For a service event handler in which file should I put the xml for the hook?
What name should I use for any request (I want to work with all requests). To put my class I need to know exactly where is <sdk_root>.

Thanks for your help,

Alejandro Barrero
<sdk_root> is the root of where you have extract the plugins SDK that you downloaded. It contains several pre-existing folders to help structure and build your various plugins.
One thing that I struggle with is that I write a hook plugin I almost always need access to Liferay classes that are in the portal-impl.jar.

This JAR is part of the ROOT.WAR and therefore you cannot access it from a Hook plugin. This means that I must add my custom code in the ROOT.WAR which defeats the whole purpose of Hook plugins.

Even for a very simple custom landing page action this is the case because you need access to the LastPath class. The same goes for the WebKeys class.

Any thoughts on this? I would very much like to see these classes be available to Hook plugins.
You're absolutely right! And your help would be much appreciated in helping us to track use cases we've overlooked. Please open a ticket for each case and we'll try to take care of those as soon as possible.

I personally think the LastPath feature should be improved. It seems to be a constant barrier to developers.

Note: Please never include the portal-impl.jar in your plugins. The problems that can arise are countless and undefined. It would be better to write a small reflection wrapper around the returned object than to include this jar in your plugin, at least until we solve the class access issue.
Thank you for your reply, but I still need some direction. I created a servelet service pre hook with NetBeans; it works fine. Now I want to create a model listener event but NetBeans doesn´t have that option. The question is how do I create a model listener event hook? What files do I have to modify as related to the servlet service event and how should they get modified?
Portal Pack has support for Model listener , Please check following blog page which has details about how to add hook for operation model.

check section : Model Listener Hook

http://www.liferay.com/web/chetan/blog/-/blogs/2580490
What about adding context.xml with
<Context>
<Loader
loaderClass="com.liferay.support.tomcat.loader.PortalClassLoader"
/>
</Context>

It works fine with Struts portlet used in Plugins SDK to access core classes. will it not work with Hooks.

Also curious to know , is it recommended solution or do you see issue with it?