Custom Velocity Tools

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!

Blogs
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</value>
</list>
</property>
</bean>
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?
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>
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.
Thanks alot for your fast answer Ray.

During the build-service task I'm getting the following error stack:
[java] 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
[java] at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(AbstractBeanFactory.java:1138)
[java] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:522)
[java] at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1174)
[java] at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:754)
[java] at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:422)
[java] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:729)
[java] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:381)
[java] at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
[java] at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:93)
[java] at com.liferay.portal.spring.context.ArrayApplicationContext.<init>(ArrayApplicationContext.java:40)
[java] at com.liferay.portal.spring.util.SpringUtil.getContext(SpringUtil.java:52)
[java] at com.liferay.portal.bean.BeanLocatorImpl.locate(BeanLocatorImpl.java:59)
[java] at com.liferay.portal.kernel.bean.PortalBeanLocatorUtil.locate(PortalBeanLocatorUtil.java:58)
[java] at com.liferay.portal.kernel.util.FileUtil._getUtil(FileUtil.java:295)
[java] at com.liferay.portal.kernel.util.FileUtil.getFile(FileUtil.java:132)
[java] at com.liferay.portal.kernel.util.FileUtil.exists(FileUtil.java:98)
[java] at com.liferay.portal.tools.servicebuilder.ServiceBuilder.<init>(ServiceBuilder.java:581)
[java] at com.liferay.portal.tools.servicebuilder.ServiceBuilder.<init>(ServiceBuilder.java:392)
[java] at com.liferay.portal.tools.servicebuilder.ServiceBuilder.main(ServiceBuilder.java:162)
[java] Caused by: java.lang.ClassNotFoundException: be.aca.tools.UserToolImpl
[java] at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
[java] at java.security.AccessController.doPrivileged(Native Method)
[java] at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
[java] at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
[java] at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:276)
[java] at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
[java] at org.springframework.util.ClassUtils.forName(ClassUtils.java:249)
[java] at org.springframework.beans.factory.support.AbstractBeanDefinition.resolveBeanClass(AbstractBeanDefinition.java:381)
[java] at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(AbstractBeanFactory.java:1135)
[java] ... 18 more

Note: I've put all my classes in the "be.aca.tools" package of my plugin
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!
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:
[java] Loading jar:file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/lib/portal-impl.jar!/system.properties
[java] Loading file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/classes/system-ext.properties
[java] Loading jar:file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/lib/portal-impl.jar!/portal.properties
[java] Loading file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/classes/portal-ext.properties
[java] 08:32:18,867 INFO [DialectDetector:65] Determining dialect for MySQL 5
[java] 08:32:18,977 INFO [DialectDetector:98] Using dialect org.hibernate.dialect.MySQLDialect
[java] Loading jar:file:/C:/Projects/Liferay/Liferay%205.2/tomcat-5.5.27/webapps/ROOT/WEB-INF/lib/portal-impl.jar!/captcha.properties
[java] 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/
[java] 08:32:32,947 INFO [ServerDetector:76] Detected server null
[java] Building ACA
[java] Writing docroot\WEB-INF\src\be\aca\tools\service\base\ACALocalServiceBaseImpl.java
[java] Writing docroot\WEB-INF\src\be\aca\tools\service\impl\ACALocalServiceImpl.java
[java] Writing docroot\WEB-INF\src\be\aca\tools\service\ACALocalService.java
[java] Writing docroot\WEB-INF\src\be\aca\tools\service\ACALocalServiceFactory.java
[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...
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:
[java] Loading jar:file:/C:/Projects/Liferay/Liferay511/webapps/ROOT/WEB-INF/lib/portal-impl.jar!/system.properties
[java] Manually loading Spring context
[java] Loading jar:file:/C:/Projects/Liferay/Liferay511/webapps/ROOT/WEB-INF/lib/portal-impl.jar!/portal.properties
[java] Loading file:/C:/Projects/Liferay/Liferay511/webapps/ROOT/WEB-INF/classes/portal-ext.properties
[java] Building ACA
[java] java.lang.StringIndexOutOfBoundsException: String index out of range: -1
[java] at java.lang.String.substring(String.java:1938)
[java] at com.liferay.portal.tools.servicebuilder.ServiceBuilder._createHBMXML(ServiceBuilder.java:1622)
[java] at com.liferay.portal.tools.servicebuilder.ServiceBuilder.<init>(ServiceBuilder.java:947)
[java] at com.liferay.portal.tools.servicebuilder.ServiceBuilder.<init>(ServiceBuilder.java:392)
[java] at com.liferay.portal.tools.servicebuilder.ServiceBuilder.main(ServiceBuilder.java:162)
BUILD SUCCESSFUL
Total time: 4 seconds

Any idea what's going on here?
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] 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....
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.
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.
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.
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
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
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.
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
Hey Ray,

I'm facing the same issue as described by Peter. Do you know which class can I use for replace BeanInterceptor?
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.
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
Hi, I wanted to use the static methods of the class

FriendlyURLNormalizer.

public static String normalize(String friendlyURL);
## >= 6.1
#set ($fun = $portal.getClass().forName("com.liferay.portal.kernel.util.FriendlyURLNormalizerUtil"))

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

$fun.normalize(...)

PS: I like the acronym!
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