« Back

Custom Velocity Tools

Company Blogs February 18, 2009 By Ray Augé Staff

Last post I talked about creating a wrapper to expose core functionality out to plugins. This time I'm going to leverage the same technique to allow you to make a custom tool available to Velocity templates without the need to edit any core classes.

The first step is to write your Tool or a wrapper for your tool following the Dependency Injection pattern.

Let's start with the interface:

package com.mytool;

public interface MyTool {

	public String operationOne();

	public String operationTwo(String name);

}

The util class:

package com.mytool;

public class MyToolUtil {

	public static MyTool getMyTool() {
		return _myTool;
	}

	public String operationOne() {
		return getMyTool().operationOne();
	}

	public String operationTwo(String name) {
		return getMyTool().operationTwo(name);
	}

	public void setMyTool(MyTool myTool) {
		_myTool = myTool;
	}

	private static MyTool _myTool;

}

The implementation class:

package com.mytool;

public class MyToolImpl implements MyTool {

	public String operationOne() {
		return "Hello out there!";
	}

	public String operationTwo(String name) {
		return "Hello " + name + "!";
	}

}

Finally, we need to wire it all together. To do that create a src/META-INF/ext-spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
	<bean id="velocityUtilInterceptor" class="com.liferay.portal.spring.aop.BeanInterceptor">
		<property name="exceptionSafe" value="true" />
	</bean>
	<bean id="baseVelocityUtil" abstract="true">
		<property name="interceptorNames">
			<list>
				<value>velocityUtilInterceptor</value>
			</list>
		</property>
	</bean>

	<bean id="com.mytool.MyTool" class="com.mytool.MyToolImpl" />
	<bean id="com.mytool.MyToolUtil" class="com.mytool.MyToolUtil">
		<property name="myTool" ref="com.mytool.MyTool" />
	</bean>
	<bean id="com.mytool.MyToolUtil.velocity" class="org.springframework.aop.framework.ProxyFactoryBean" parent="baseVelocityUtil">
		<property name="target" ref="com.mytool.MyTool" />
	</bean>
</beans>

Now we're ready to use our tool in a Velocity template:

#set ($myTool = $utilLocator.findTool('com.mytool.MyToolUtil'))

$myTool.operationOne()

$myTool.operationTwo('Ray')

If you happened to define this in a ServiceBuilder enabled plugin, you will have to specify the 'contextPathName' of the plugin so that the appropriate classloader is used to lookup your tool. For example, the context path name of your plugin being "my-tool-portlet", then:

#set ($myTool = $utilLocator.findTool('my-tool-portlet', 'com.mytool.MyToolUtil'))

$myTool.operationOne()

$myTool.operationTwo('Ray')

Enjoy!

Threaded Replies Author Date
Sweet! Thank you, Ray! Jonas Yuan February 19, 2009 3:07 AM
Hi Ray, The following code (spring beans)... Jonas Yuan February 19, 2009 8:47 AM
No need! Ray Augé March 26, 2009 7:40 AM
When trying to get a reference to my custom... Peter Mesotten March 26, 2009 7:17 AM
Hmm, strange. Can you describe when and where... Ray Augé March 26, 2009 7:41 AM
I'm working with 5.2.1. I created a simple web... Peter Mesotten March 26, 2009 8:21 AM
AHH!... My fault. I should have stated that the... Ray Augé March 26, 2009 8:37 AM
Thanks alot for your fast answer Ray. During... Peter Mesotten March 26, 2009 8:53 AM
> Note: I've put all my classes in the... Ray Augé March 26, 2009 9:01 AM
They're inside WEB-INF/src indeed. Peter Mesotten March 27, 2009 12:39 AM
It works! I'm just stupid... I of course had to... Peter Mesotten March 27, 2009 12:49 AM
Hm I was too fast. On the GUI, the same error... Peter Mesotten March 27, 2009 1:32 AM
I've seen this before. Do you have clustering... Ray Augé March 30, 2009 5:42 AM
Nope, no clustering. Just working locally with... Peter Mesotten March 30, 2009 7:14 AM
That's really strange! Because I just tested in... Ray Augé March 30, 2009 7:33 AM
Just tested in trunk. Just added this: <?xml... Ray Augé March 30, 2009 7:49 AM
PS: I just made a small change which (when it... Ray Augé March 30, 2009 7:50 AM
Wow that's strange.. You chose "be.aca" as... Peter Mesotten March 30, 2009 8:03 AM
5.1. does indeed have $utilLocator. Ray Augé March 30, 2009 8:16 AM
You mean it doesn't exist? Because I can't find... Peter Mesotten March 31, 2009 3:55 AM
http://svn.liferay.com/browse/portal/branches/5... Ray Augé March 31, 2009 6:45 AM
I just tried building & deploying using the 5.2... Peter Mesotten March 31, 2009 4:35 AM
You have to use the matching SDK for the portal... Ray Augé March 31, 2009 6:59 AM
Hello Ray, Any ideas on how to integrate this... Peter Fox March 14, 2010 7:39 AM
Hey Ray, I'm facing the same issue as... Thiago Leão Moreira September 30, 2010 1:32 PM
Hey guys, sorry for the long silence. I finally... Ray Augé October 4, 2010 6:45 AM
Ray, Your velocity hook was working until very... Ian White October 19, 2011 3:12 PM
Hi, I wanted to use the static methods of the... David García González March 1, 2012 4:46 AM
## >= 6.1 #set ($fun =... Ray Augé March 1, 2012 9:10 AM
Hi Ray! Can i call custom classes with this... Riccardo Masini July 11, 2012 3:46 AM
Thanks good practice Mohammad Hejazi November 15, 2014 12:10 AM

Sweet! Thank you, Ray!
Posted on 2/19/09 3:07 AM.
Hi Ray,

The following code (spring beans) exists in util-spring.xml. Do we need to specify them again in /src/META-INF/ext-spring.xml?

Thanks

Jonas Yuan

----

<bean id="velocityUtilInterceptor" class="com.liferay.portal.spring.aop.BeanInterceptor">
<property name="exceptionSafe" value="true" />
</bean>
<bean id="baseVelocityUtil" abstract="true">
<property name="interceptorNames">
<list>
<value>velocityUtilInterceptor</va­lue>
</list>
</property>
</bean>
Posted on 2/19/09 8:47 AM.
When trying to get a reference to my custom util class, my VM parser shows this error:

Invocation of method 'findUtil' in class com.liferay.portal.velocity.UtilLocator threw exception com.liferay.portal.kernel.bean.BeanLocatorException: BeanLocator has not been set at com.liferay.portlet.journal.util.JournalVmUtil[line 22, column 14]

Any idea what this means?
Posted on 3/26/09 7:17 AM.
Posted on 3/26/09 7:40 AM in reply to Jonas Yuan.
Hmm, strange. Can you describe when and where your template is being called?
Posted on 3/26/09 7:41 AM in reply to Peter Mesotten.
I'm working with 5.2.1.
I created a simple web content article. In my template I call:

$utilLocator.findUtil("custom-utils", "be.aca.tools.util.UserToolUtil")

When I navigate to the article on my page, the previously mentioned error shows together with the velocity code itself. In the catalina.out i get: BeanLocator is null.

I created a project (custom-utils) in the plugins SDK with IUserTool, UserToolImpl and UserToolUtil. My ext-spring.xml looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
<bean id="velocityUtilInterceptor" class="com.liferay.portal.spring.aop.BeanInterceptor">
<property name="exceptionSafe" value="true"/>
</bean>
<bean id="baseVelocityUtil" abstract="true">
<property name="interceptorNames">
<list>
<value>velocityUtilInterceptor</value>
</list>
</property>
</bean>

<bean id="be.aca.tools.ifc.IUserTool" class="be.aca.tools.impl.UserToolImpl"/>
<bean id="be.aca.tools.util.UserToolUtil" class="be.aca.tools.util.UserToolUtil">
<property name="userTool" ref="be.aca.tools.ifc.IUserTool"/>
</bean>
<bean id="be.aca.tools.util.UserToolUtil.velocity" class="org.springframework.aop.framework.ProxyFactoryBean" parent="baseVelocityUtil">
<property name="target" ref="be.aca.tools.ifc.IUserTool"/>
</bean>
</beans>
Posted on 3/26/09 8:21 AM.
AHH!... My fault. I should have stated that the ability to inject from a plugin also requires ServiceBuilder.

So, the simplest solution is to create a service.xml file in your plugin with a single entity having no columns defined, like:

<service-builder package-path="be.aca">
<namespace>ACA</namespace>
<entity name="ACA" local-service="true" remote-service="false" />
</service-builder>

then do ant build-service.

Now your ext-spring file should be read, it isn't being read currently.
Posted on 3/26/09 8:37 AM in reply to Peter Mesotten.
Thanks alot for your fast answer Ray.

During the build-service task I'm getting the following error stack:
org.springframework.beans.factory.CannotLoadBeanClassException: Cannot find class [be.aca.tools.UserToolImpl] for bean with name 'be.aca.tools.IUserTool' defined in class path resource [META-INF/ext-spring.xml]; nested exception is java.lang.ClassNotFoundException: be.aca.tools.UserToolImpl
at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(A­bstractBeanFactory.java:1138)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.pre­dictBeanType(AbstractAutowireCapableBeanFactory.java:522)
at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(Abst­ractBeanFactory.java:1174)
at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(Abst­ractBeanFactory.java:754)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstanti­ateSingletons(DefaultListableBeanFactory.java:422)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactory­Initialization(AbstractApplicationContext.java:729)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractA­pplicationContext.java:381)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassP­athXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassP­athXmlApplicationContext.java:93)
at com.liferay.portal.spring.context.ArrayApplicationContext.<init>(ArrayApplicatio­nContext.java:40)
at com.liferay.portal.spring.util.SpringUtil.getContext(SpringUtil.java:52)
at com.liferay.portal.bean.BeanLocatorImpl.locate(BeanLocatorImpl.java:59)
at com.liferay.portal.kernel.bean.PortalBeanLocatorUtil.locate(PortalBeanLocatorUti­l.java:58)
at com.liferay.portal.kernel.util.FileUtil._getUtil(FileUtil.java:295)
at com.liferay.portal.kernel.util.FileUtil.getFile(FileUtil.java:132)
at com.liferay.portal.kernel.util.FileUtil.exists(FileUtil.java:98)
at com.liferay.portal.tools.servicebuilder.ServiceBuilder.<init>(ServiceBuilder.jav­a:581)
at com.liferay.portal.tools.servicebuilder.ServiceBuilder.<init>(ServiceBuilder.jav­a:392)
at com.liferay.portal.tools.servicebuilder.ServiceBuilder.main(ServiceBuilder.java:­162)
Caused by: java.lang.ClassNotFoundException: be.aca.tools.UserToolImpl
at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:276)
at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
at org.springframework.util.ClassUtils.forName(ClassUtils.java:249)
at org.springframework.beans.factory.support.AbstractBeanDefinition.resolveBeanClas­s(AbstractBeanDefinition.java:381)
at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(A­bstractBeanFactory.java:1135)
... 18 more

Note: I've put all my classes in the "be.aca.tools" package of my plugin
Posted on 3/26/09 8:53 AM.
> Note: I've put all my classes in the "be.aca.tools" package of my plugin

And those are in WEB-INF/src/ ? or in a jar in WEB-INF/lib/ ?
Posted on 3/26/09 9:01 AM in reply to Peter Mesotten.
They're inside WEB-INF/src indeed.
Posted on 3/27/09 12:39 AM.
It works! I'm just stupid... I of course had to ant compile my source files first before running build-service, because my source files were moved. Now, the beans are correctly created and the service is built.

Thanks alot!
Posted on 3/27/09 12:49 AM.
Hm I was too fast. On the GUI, the same error still arises. I debugged in PortletBeanLocatorUtil and saw that the _beanLocators map only has a value for the wol-portlet...

Also, I had pointed my ant scripts to the wrong tomcat server. Now, when I build-service, the script output shows:
Buildfile: C:\Workspaces\liferay\Liferay\hooks\custom-utils\build.xml
build-service:
Loading jar:file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/l­ib/portal-impl.jar!/system.properties
Loading file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/class­es/system-ext.properties
Loading jar:file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/l­ib/portal-impl.jar!/portal.properties
Loading file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/class­es/portal-ext.properties
08:32:18,867 INFO [DialectDetector:65] Determining dialect for MySQL 5
08:32:18,977 INFO [DialectDetector:98] Using dialect org.hibernate.dialect.MySQLDialect
Loading jar:file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/l­ib/portal-impl.jar!/captcha.properties
08:32:23,852 INFO [PortalImpl:237] Portal lib directory /C:/Projects/Liferay/Liferay 5.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/lib/
08:32:32,947 INFO [ServerDetector:76] Detected server null
Building ACA
Writing docroot\WEB-INF\src\be\aca\tools\service\base\ACALocalServiceBaseImpl.java
Writing docroot\WEB-INF\src\be\aca\tools\service\impl\ACALocalServiceImpl.java
Writing docroot\WEB-INF\src\be\aca\tools\service\ACALocalService.java
Writing docroot\WEB-INF\src\be\aca\tools\service\ACALocalServiceFactory.java
Writing docroot\WEB-INF\src\be\aca\tools\service\ACALocalServiceUtil.java

After this last line, the script seems to pause so I have to manually stop it.. So I suppose the service is not correctly built...
Posted on 3/27/09 1:32 AM.
I've seen this before. Do you have clustering enabled? If so, disable it when trying to build.
Posted on 3/30/09 5:42 AM in reply to Peter Mesotten.
Nope, no clustering. Just working locally with one instance..

I just tried the same on my Liferay 5.1.1 instance (the version we're using on our production server). The build-service task now outputs the following:

Buildfile: C:\Workspaces\liferay\Liferay\hooks\custom-utils\build.xml
build-service:
Loading jar:file:/C:/Projects/Liferay/Liferay511/webapps/ROOT/WEB-INF/lib/portal-impl.ja­r!/system.properties
Manually loading Spring context
Loading jar:file:/C:/Projects/Liferay/Liferay511/webapps/ROOT/WEB-INF/lib/portal-impl.ja­r!/portal.properties
Loading file:/C:/Projects/Liferay/Liferay511/webapps/ROOT/WEB-INF/classes/portal-ext.pro­perties
Building ACA
java.lang.StringIndexOutOfBoundsException: String index out of range: -1
at java.lang.String.substring(String.java:1938)
at com.liferay.portal.tools.servicebuilder.ServiceBuilder._createHBMXML(ServiceBuil­der.java:1622)
at com.liferay.portal.tools.servicebuilder.ServiceBuilder.<init>(ServiceBuilder.jav­a:947)
at com.liferay.portal.tools.servicebuilder.ServiceBuilder.<init>(ServiceBuilder.jav­a:392)
at com.liferay.portal.tools.servicebuilder.ServiceBuilder.main(ServiceBuilder.java:­162)
BUILD SUCCESSFUL
Total time: 4 seconds

Any idea what's going on here?
Posted on 3/30/09 7:14 AM.
That's really strange! Because I just tested in 5.1.x with:

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 5.1.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_5_1_0.dtd">

<service-builder package-path="be.aca">
<namespace>ACA</namespace>
<entity name="ACA" local-service="true" remote-service="false" />
</service-builder>

and got no " java.lang.StringIndexOutOfBoundsException: String index out of range: -1"

I didn't test on 5.2.x as I don't have a build of that.. testing trunk....
Posted on 3/30/09 7:33 AM in reply to Peter Mesotten.
Just tested in trunk. Just added this:

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 5.2.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_5_2_0.dtd">

<service-builder package-path="be.aca">
<namespace>ACA</namespace>
<entity name="ACA" local-service="true" remote-service="false" />
</service-builder>

to a portlet without any service and ran "ant build-service" and no issues.
Posted on 3/30/09 7:49 AM in reply to Ray Augé.
PS: I just made a small change which (when it gets backported to 5.1.x) will allow you to skip the ServiceBuilder part and just use the spring portion.

Will Blog on that later.
Posted on 3/30/09 7:50 AM in reply to Ray Augé.
Wow that's strange.. You chose "be.aca" as service path, while I picked "be.aca.tools", the class where all util/class/ifc files are situated. And with your path, it worked!

However, 5.1 does not have $utilLocator so I can't test if it works on the UI side. Or is there any other way to access custom util classes in 5.1?

In 5.2, the exact same problem still exists after editing the service path.
Posted on 3/30/09 8:03 AM in reply to Ray Augé.
5.1. does indeed have $utilLocator.
Posted on 3/30/09 8:16 AM in reply to Peter Mesotten.
You mean it doesn't exist? Because I can't find it anywhere in the 5.1 source and it is not defined in VelocityVariables.java
Posted on 3/31/09 3:55 AM.
I just tried building & deploying using the 5.2 Plugins SDK (I used the 5.1 SDK until now). Both building the service and deploying on the server go fine with no errors. But on the UI, I'm still getting the same error:

Invocation of method 'findUtil' in class com.liferay.portal.velocity.UtilLocator threw exception com.liferay.portal.kernel.bean.BeanLocatorException: BeanLocator has not been set at com.liferay.portlet.journal.util.JournalVmUtil[line 22, column 14]

And in my catalina.out:
11:31:01,958 ERROR [PortletBeanLocatorUtil:52] BeanLocator is null
Posted on 3/31/09 4:35 AM.
http://svn.liferay.com/browse/portal/branches/5.1.x/portal-impl/src/com/liferay/­portal/util/UtilLocator.java
Posted on 3/31/09 6:45 AM in reply to Peter Mesotten.
You have to use the matching SDK for the portal version. Once you get that sorted out, make sure that you have the correct context path name to access the tool in your plugin:

#set ($myTool = $utilLocator.findUtil('my-tool-portlet', 'com.mytool.MyToolUtil'))

where "my-tool-portlet" is the name of the webapp folder of the plugin.

And this most definitely works in 5.1, as I'm currently on a 5.1 based project and we're using several custom velocity tools.
Posted on 3/31/09 6:59 AM in reply to Peter Mesotten.
Hello Ray,

Any ideas on how to integrate this approach with the new awesome Liferay 6 RC?

I'm trying migrate code from 5.2.x to liferay6 and I defined custom velocity tool from a ext-plugin.
I've checked and spring beans are being created/loaded

Currently class: com.liferay.portal.spring.aop.BeanInterceptor does not exist on liferay6 but tried to plug it into my custom-ext plugin.

I've checked and can see that utilLocator.find("customService") is working and I can see the proxy

...problem is that MethodInterceptor for some reason is not being invoked..

#set ($myTool = $utilLocator.findUtil('com.mytool.MyToolUtil')) -->ok
$myTool.operationOne() --> does nothing! nok

This aproach is not supported on liferay6 ?... should I use ServiceHookAdvice instead?

Thanks in advance..appreciate your help.

regards,

Peter
Posted on 3/14/10 7:39 AM.
Hey Ray,

I'm facing the same issue as described by Peter. Do you know which class can I use for replace BeanInterceptor?
Posted on 9/30/10 1:32 PM in reply to Peter Fox.
Hey guys, sorry for the long silence. I finally got around to trying to figure out what the problem was.

So, you can still do this, but there is one change in 6.0. Your util class is wrapped in a proxy object to capture exceptions. The result of this wrapping is that your outer class must implement an interface. I will write a new blog to explain.
Posted on 10/4/10 6:45 AM in reply to Thiago Leão Moreira.
Ray,

Your velocity hook was working until very recently when I finally decided to do an svn update from portal trunk.

Now, the bean defined in the applicationContext.xml is not being found.

Are you able to confirm if the velocity hook functionality has been removed ?

Thanks

Ian
Posted on 10/19/11 3:12 PM in reply to Ray Augé.
Hi, I wanted to use the static methods of the class

FriendlyURLNormalizer.

public static String normalize(String friendlyURL);
Posted on 3/1/12 4:46 AM in reply to Ian White.
## >= 6.1
#set ($fun = $portal.getClass().forName("com.liferay.portal.kernel.util.FriendlyURLNormalizer­Util"))

## <= 6.0
#set ($fun = $portal.getClass().forName("com.liferay.portal.util.FriendlyURLNormalizer"))

$fun­.normalize(...)

PS: I like the acronym!
Posted on 3/1/12 9:10 AM in reply to David García González.
Hi Ray! Can i call custom classes with this method in liferay 5.2.2?
I have 3 classes i use to extract some files to the Document Library reading tags, but i can't use those classes in velocity macro....
Regards
Posted on 7/11/12 3:46 AM in reply to Ray Augé.
Thanks
good practice
Posted on 11/15/14 12:10 AM.