Recent Bloggers

Joshua Asbury

Staff
21 Mensagens
23 de Maio de 2013

Liferay Support Portal Admin

Staff
57 Mensagens
23 de Maio de 2013

Navin Agarwal

2 Mensagens
22 de Maio de 2013

Olaf Kock

Staff
51 Mensagens
21 de Maio de 2013

Ronald Sarayudej

Staff
149 Mensagens
20 de Maio de 2013

Roberto Díaz

Staff
1 Mensagens
16 de Maio de 2013

Miguel Pastor Olivar

Staff
8 Mensagens
14 de Maio de 2013

Kamesh Sampath

5 Mensagens
13 de Maio de 2013

Richard Sezov

Staff
38 Mensagens
13 de Maio de 2013

Kan Zhang

6 Mensagens
13 de Maio de 2013

Fancy Frameworks: A surefire way of consuming all the memory of any portlet container...

Staff Blogs 5 de Março de 2009 Por Ray Augé Staff

Perhaps as a secondary title I should put: "Why is every Liferay plugin writtin in plain old JSP?".

There is a very big trend toward wanting to build portlets using some form of fancy framework. It is a seductive notion. One filled with all manner of "potential" benefits.

There is also a very dangerous side effect. One that we don't initially consider, at least not until too much time and money has been spent. The problem is the design of the java classloader.

While great for security it forces each WAR to load it's own version of classes into the VM, these are not shared between apps. We all know this, right?

In the case of portals, this becomes a little bit of a problem because you tend to want to deploy portlets as separate bundles (WARs). But imagine what happens if you installed a whole bunch of individually packaged portlets which all use heavy frameworks. This will start to consume all the available perm gen space with class meta data.

You might wonder "Why is Liferay so slow?" But I assure you, the same problems would happen with a whole bunch of regular old webapps all using heavy frameworks.

If all the Liferay plugins used Spring MVC for instance, (this is not a knock at Spring MVC, it's a great framework, I'm just using it as an example, the same issue would arrise with any JSF or other heavy framework) by the time you reached the 7th or 8th plugin you might start to notice the effect these are having on the VM's memory consumption.

If your project is planing for lots separation (as Liferay's plugins are generally designed), consider using the lightest possible framework, or using a framework that you can share globally between the plugins (as is the case with JSP, and some Liferay specific portlet bridges, and why we use plain old JSP almost exclusively).

I don't like the idea of building huge bundles, BUT in the case of heavy design frameworks you may find you have no choice but to bundle portlets into groups in order to save on memory.

So keep this in mind!

What if Liferay could return your customized entity from an existing service

Staff Blogs 5 de Março de 2009 Por Ray Augé Staff

Wouldn't that be a hoot? But how do we do that? Simple, we use AOP (Apsect Oriented Programming). Ok, you tried that but couldn't make it work with Liferay. Well, that's what this particular post is all about. I'll show you how in three simple steps.

Ok, so first off your custom entity MUST extend the entity of the service in question.

Here is an example of an entity which extends Layout(Impl, extend the Impl always.. otherwise you will have problems):

package com.ray.portal.model.impl;

import ...

public class VersionLayoutImpl extends LayoutImpl {

	// Copy All these methods from *ModelImpl of the entity you are extending
 	// and adjust as needed for your custom type

	public static Layout toModel(LayoutSoap soapModel) {
		VersionLayoutImpl model = new VersionLayoutImpl();

		model.setPlid(soapModel.getPlid());
		...

		return model;
	}

	public Layout toEscapedModel() {
		if (isEscapedModel()) {
			return (Layout)this;
		}
		else {
			Layout model = new VersionLayoutImpl();

			model.setNew(isNew());
			...

			return model;
		}
	}

	public Object clone() {
		VersionLayoutImpl clone = new VersionLayoutImpl();

		clone.setPlid(getPlid());
		...

		return clone;
	}

	public static VersionLayoutImpl clone(LayoutModelImpl layout) {
		VersionLayoutImpl clone = new VersionLayoutImpl();

		clone.setPlid(layout.getPlid());
		...
		
		return clone;
	}

	public int compareTo(Object obj) {
		...
	}

	public boolean equals(Object obj) {
		...	
	}

}

We also added a static clone method to make it easier to go from the stock entity to the custom one.

Next, you need to replace the returned types from the existing service, so you need some way to inject your types in their place. To do this write a pretty simple org.aopalliance.intercept.MethodInterceptor implementation like so:

package com.ray.portal.service.aop;

import com.liferay.portal.model.impl.LayoutModelImpl;

import java.util.ArrayList;
import java.util.List;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import com.ray.portal.model.impl.VersionLayoutImpl;

public class LayoutLocalServiceInterceptor implements MethodInterceptor {

	public Object invoke(MethodInvocation invocation) throws Throwable {
		Object result = invocation.proceed();

		if (result instanceof LayoutModelImpl) {
			result = wrapLayout(result);
		}
		else if (result instanceof List) {
			result = new WrappedList((List<LayoutModelImpl>)result);
		}

		return result;
	}

	protected VersionLayoutImpl wrapLayout(Object object) {
		if (object instanceof VersionLayoutImpl) {
			return (VersionLayoutImpl)object;
		}

		return VersionLayoutImpl.clone((LayoutModelImpl)object);
	}

	public class WrappedList extends ArrayList<LayoutModelImpl> {

		public WrappedList(List<? extends LayoutModelImpl> list) {
			super(list);
		}

		public VersionLayoutImpl get(int index) {
			return wrapLayout(super.get(index));
		}

	}

}

Pretty simple right? All we need to do now is wire this interceptor into the IOC container and we're all set.

As usual we make a visit to our friend ext-spring.xml and add:

	<aop:config>
		<aop:pointcut id="versionLayoutOperation" expression="bean(com.liferay.portal.service.LayoutLocalService.impl)" />
		<aop:advisor advice-ref="versionLayoutInterceptor" pointcut-ref="versionLayoutOperation" />
	</aop:config>
	<bean id="versionLayoutInterceptor" class="com.ray.portal.service.aop.LayoutLocalServiceInterceptor" />

All done! So, you can add/override methods on your entity and othewise customize away.

Cool eh? Enjoy!

Custom Velocity Tools

Staff Blogs 18 de Fevereiro de 2009 Por 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!

Performance testing using a nifty portal tool

Staff Blogs 2 de Fevereiro de 2009 Por Ray Augé Staff

We all want to optimize the processing time of our applications. To do so we use all kinds of different tools: HTTP load producers to measure response times from a client perspective, profilers for memory and thread loading, are two of the most important. Sometimes those can be a chore to use and setup. Sometimes they don't give fine grained enough information or perhaps just not the right perspective. Perhaps you need one more tool. After all, the more tools you have the better equiped you are. Problem is you need to also know how to use it.

A few iterations of Liferay back, we created tool for testing the configuration of our many servlet filters, while also giving valuable information like the time taken by the [app server|servlet container] to handle any portal request.

This tool is simply a logging configuration that pumps out lots of information w.r.t. to each servlet filter, and since every request handled by the portal passes through at the very least one filter we also gain processing time taken during the handling of that filter.

Here is an example logging configuration using portal-log4j-ext.xml:

	<category name="com.liferay.portal.servlet.filters.autologin.AutoLoginFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.cache.CacheFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.doubleclick.DoubleClickFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.gzip.GZipFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.header.HeaderFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.minifier.MinifierFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.secure.SecureFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.servletauthorizing.ServletAuthorizingFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.sessionid.SessionIdFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.strip.StripFilter">
		<priority value="DEBUG" />
	</category>
	<category name="com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter">
		<priority value="DEBUG" />
	</category>

You don't need all those filters configured, but this gives you the most information and will make the most sense.

Here is an example of the raw output:

09:40:55,411 DEBUG [SessionIdFilter:40] [http-8080-3]> com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /user/test/1
09:40:55,411 DEBUG [VirtualHostFilter:40] class com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter is enabled
09:40:55,412 DEBUG [VirtualHostFilter:40] Company id 10095
09:40:55,412 DEBUG [VirtualHostFilter:40] Received http://localhost:8080/user/test/1
09:40:55,412 DEBUG [VirtualHostFilter:40] Friendly URL /user/test/1
09:40:55,412 DEBUG [VirtualHostFilter:40] [http-8080-3]=> com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /user/test/1
09:40:55,412 DEBUG [OpenSSOFilter:40] class com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter is enabled
09:40:55,413 DEBUG [OpenSSOFilter:40] [http-8080-3]==> com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter /user/test/1
09:40:55,413 DEBUG [AutoLoginFilter:40] class com.liferay.portal.servlet.filters.autologin.AutoLoginFilter is enabled
09:40:55,414 DEBUG [AutoLoginFilter:40] [http-8080-3]===> com.liferay.portal.servlet.filters.autologin.AutoLoginFilter /user/test/1
09:40:55,414 DEBUG [CacheFilter:40] class com.liferay.portal.servlet.filters.cache.CacheFilter is enabled
09:40:55,415 DEBUG [CacheFilter:40] Request is not cacheable HTTP:///USER/test/1?NULL#EN_US#OTHER#TRUE
09:40:55,416 DEBUG [CacheFilter:40] [http-8080-3]====> com.liferay.portal.servlet.filters.cache.CacheFilter /user/test/1
09:40:55,416 DEBUG [DoubleClickFilter:40] class com.liferay.portal.servlet.filters.doubleclick.DoubleClickFilter is disabled
09:40:55,416 DEBUG [DoubleClickFilter:40] [http-8080-3]=====> com.liferay.portal.servlet.filters.doubleclick.DoubleClickFilter /user/test/1
09:40:55,417 DEBUG [SecureFilter:40] class com.liferay.portal.servlet.filters.secure.SecureFilter is enabled
09:40:55,417 DEBUG [SecureFilter:40] Access allowed for 127.0.0.1
09:40:55,417 DEBUG [SecureFilter:40] https is not required
09:40:55,417 DEBUG [SecureFilter:40] Not securing http://localhost:8080/user/test/1
09:40:55,418 DEBUG [SecureFilter:40] [http-8080-3]======> com.liferay.portal.servlet.filters.secure.SecureFilter /user/test/1
09:40:55,418 DEBUG [GZipFilter:40] class com.liferay.portal.servlet.filters.gzip.GZipFilter is enabled
09:40:55,418 DEBUG [GZipFilter:40] Compressing http://localhost:8080/user/test/1
09:40:55,419 DEBUG [GZipFilter:40] [http-8080-3]=======> com.liferay.portal.servlet.filters.gzip.GZipFilter /user/test/1
09:40:55,419 DEBUG [StripFilter:40] class com.liferay.portal.servlet.filters.strip.StripFilter is enabled
09:40:55,419 DEBUG [StripFilter:40] Stripping http://localhost:8080/user/test/1
09:40:55,419 DEBUG [StripFilter:40] [http-8080-3]========> com.liferay.portal.servlet.filters.strip.StripFilter /user/test/1
09:40:56,639 WARN  [PortletLocalServiceImpl:164] Portlet not found for 10095 BrowseAndPlayContents_WAR_sesameportlets4369
09:40:56,641 WARN  [PortletLocalServiceImpl:164] Portlet not found for 10095 65
09:41:03,857 INFO  [PluginPackageUtil:76] Checking for available updates
09:41:03,883 INFO  [PluginPackageUtil:76] Finished checking for available updates in 26 ms
09:41:03,925 WARN  [PortletLocalServiceImpl:164] Portlet not found for 10095 BrowseAndPlayContents_WAR_sesameportlets4369
09:41:03,926 WARN  [PortletLocalServiceImpl:164] Portlet not found for 10095 65
09:41:04,685 DEBUG [StripFilter:40] [http-8080-3]========< com.liferay.portal.servlet.filters.strip.StripFilter /user/test/1 9266 ms
09:41:04,686 DEBUG [StripFilter:40] Stripping content of type text/html; charset=utf-8
09:41:04,714 DEBUG [GZipFilter:40] [http-8080-3]=======< com.liferay.portal.servlet.filters.gzip.GZipFilter /user/test/1 9295 ms
09:41:04,714 DEBUG [SecureFilter:40] [http-8080-3]======< com.liferay.portal.servlet.filters.secure.SecureFilter /user/test/1 9296 ms
09:41:04,715 DEBUG [DoubleClickFilter:40] [http-8080-3]=====< com.liferay.portal.servlet.filters.doubleclick.DoubleClickFilter /user/test/1 9299 ms
09:41:04,715 DEBUG [CacheFilter:40] [http-8080-3]====< com.liferay.portal.servlet.filters.cache.CacheFilter /user/test/1 9299 ms
09:41:04,715 DEBUG [AutoLoginFilter:40] [http-8080-3]===< com.liferay.portal.servlet.filters.autologin.AutoLoginFilter /user/test/1 9301 ms
09:41:04,716 DEBUG [OpenSSOFilter:40] [http-8080-3]==< com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter /user/test/1 9303 ms
09:41:04,716 DEBUG [VirtualHostFilter:40] [http-8080-3]=< com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /user/test/1 9304 ms
09:41:04,716 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /user/test/1 9305 ms

Now, how do we extract valueable information out of this? Well, it's really quite simple for anyone with access to a shell console and a standard grep command.

$ grep "\[http-8080-2\]" catalina.out
09:40:43,565 DEBUG [SessionIdFilter:40] [http-8080-2]> com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/themes/classic/images/forms/button.png
09:40:43,566 DEBUG [VirtualHostFilter:40] [http-8080-2]=> com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /html/themes/classic/images/forms/button.png
09:40:43,566 DEBUG [HeaderFilter:40] [http-8080-2]==> com.liferay.portal.servlet.filters.header.HeaderFilter /html/themes/classic/images/forms/button.png
09:40:43,567 DEBUG [HeaderFilter:40] [http-8080-2]==< com.liferay.portal.servlet.filters.header.HeaderFilter /html/themes/classic/images/forms/button.png 1 ms
09:40:43,567 DEBUG [VirtualHostFilter:40] [http-8080-2]=< com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /html/themes/classic/images/forms/button.png 1 ms
09:40:43,567 DEBUG [SessionIdFilter:40] [http-8080-2]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/themes/classic/images/forms/button.png 3 ms
09:40:55,219 DEBUG [SessionIdFilter:40] [http-8080-2]> com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /c/portal/login
09:40:55,222 DEBUG [VirtualHostFilter:40] [http-8080-2]=> com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /c/portal/login
09:40:55,225 DEBUG [OpenSSOFilter:40] [http-8080-2]==> com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter /c/portal/login
09:40:55,230 DEBUG [AutoLoginFilter:40] [http-8080-2]===> com.liferay.portal.servlet.filters.autologin.AutoLoginFilter /c/portal/login
09:40:55,234 DEBUG [SecureFilter:40] [http-8080-2]====> com.liferay.portal.servlet.filters.secure.SecureFilter /c/portal/login
09:40:55,236 DEBUG [GZipFilter:40] [http-8080-2]=====> com.liferay.portal.servlet.filters.gzip.GZipFilter /c/portal/login
09:40:55,242 DEBUG [StripFilter:40] [http-8080-2]======> com.liferay.portal.servlet.filters.strip.StripFilter /c/portal/login
09:40:55,404 DEBUG [StripFilter:40] [http-8080-2]======< com.liferay.portal.servlet.filters.strip.StripFilter /c/portal/login 162 ms
09:40:55,407 DEBUG [GZipFilter:40] [http-8080-2]=====< com.liferay.portal.servlet.filters.gzip.GZipFilter /c/portal/login 171 ms
09:40:55,407 DEBUG [SecureFilter:40] [http-8080-2]====< com.liferay.portal.servlet.filters.secure.SecureFilter /c/portal/login 173 ms
09:40:55,408 DEBUG [AutoLoginFilter:40] [http-8080-2]===< com.liferay.portal.servlet.filters.autologin.AutoLoginFilter /c/portal/login 178 ms
09:40:55,408 DEBUG [OpenSSOFilter:40] [http-8080-2]==< com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter /c/portal/login 183 ms
09:40:55,408 DEBUG [VirtualHostFilter:40] [http-8080-2]=< com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /c/portal/login 186 ms
09:40:55,408 DEBUG [SessionIdFilter:40] [http-8080-2]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /c/portal/login 189 ms
09:41:04,778 DEBUG [SessionIdFilter:40] [http-8080-2]> com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/js/everything.js
09:41:04,779 DEBUG [VirtualHostFilter:40] [http-8080-2]=> com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /html/js/everything.js
09:41:04,780 DEBUG [CacheFilter:40] [http-8080-2]==> com.liferay.portal.servlet.filters.cache.CacheFilter /html/js/everything.js
09:41:04,781 DEBUG [HeaderFilter:40] [http-8080-2]===> com.liferay.portal.servlet.filters.header.HeaderFilter /html/js/everything.js
09:41:04,782 DEBUG [GZipFilter:40] [http-8080-2]====> com.liferay.portal.servlet.filters.gzip.GZipFilter /html/js/everything.js
09:41:04,956 DEBUG [GZipFilter:40] [http-8080-2]====< com.liferay.portal.servlet.filters.gzip.GZipFilter /html/js/everything.js 175 ms
09:41:04,956 DEBUG [HeaderFilter:40] [http-8080-2]===< com.liferay.portal.servlet.filters.header.HeaderFilter /html/js/everything.js 175 ms
09:41:04,957 DEBUG [CacheFilter:40] [http-8080-2]==< com.liferay.portal.servlet.filters.cache.CacheFilter /html/js/everything.js 177 ms
09:41:05,000 DEBUG [VirtualHostFilter:40] [http-8080-2]=< com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /html/js/everything.js 220 ms
09:41:05,000 DEBUG [SessionIdFilter:40] [http-8080-2]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/js/everything.js 222 ms
09:41:11,020 DEBUG [SessionIdFilter:40] [http-8080-2]> com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /user/test/1
09:41:11,025 DEBUG [VirtualHostFilter:40] [http-8080-2]=> com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /user/test/1
09:41:11,028 DEBUG [OpenSSOFilter:40] [http-8080-2]==> com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter /user/test/1
09:41:11,029 DEBUG [AutoLoginFilter:40] [http-8080-2]===> com.liferay.portal.servlet.filters.autologin.AutoLoginFilter /user/test/1
09:41:11,031 DEBUG [CacheFilter:40] [http-8080-2]====> com.liferay.portal.servlet.filters.cache.CacheFilter /user/test/1
09:41:11,033 DEBUG [DoubleClickFilter:40] [http-8080-2]=====> com.liferay.portal.servlet.filters.doubleclick.DoubleClickFilter /user/test/1
09:41:11,038 DEBUG [SecureFilter:40] [http-8080-2]======> com.liferay.portal.servlet.filters.secure.SecureFilter /user/test/1
09:41:11,041 DEBUG [GZipFilter:40] [http-8080-2]=======> com.liferay.portal.servlet.filters.gzip.GZipFilter /user/test/1
09:41:11,044 DEBUG [StripFilter:40] [http-8080-2]========> com.liferay.portal.servlet.filters.strip.StripFilter /user/test/1
09:41:11,098 DEBUG [StripFilter:40] [http-8080-2]========< com.liferay.portal.servlet.filters.strip.StripFilter /user/test/1 54 ms
09:41:11,098 DEBUG [GZipFilter:40] [http-8080-2]=======< com.liferay.portal.servlet.filters.gzip.GZipFilter /user/test/1 57 ms
09:41:11,099 DEBUG [SecureFilter:40] [http-8080-2]======< com.liferay.portal.servlet.filters.secure.SecureFilter /user/test/1 61 ms
09:41:11,099 DEBUG [DoubleClickFilter:40] [http-8080-2]=====< com.liferay.portal.servlet.filters.doubleclick.DoubleClickFilter /user/test/1 66 ms
09:41:11,099 DEBUG [CacheFilter:40] [http-8080-2]====< com.liferay.portal.servlet.filters.cache.CacheFilter /user/test/1 68 ms
09:41:11,100 DEBUG [AutoLoginFilter:40] [http-8080-2]===< com.liferay.portal.servlet.filters.autologin.AutoLoginFilter /user/test/1 71 ms
09:41:11,100 DEBUG [OpenSSOFilter:40] [http-8080-2]==< com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter /user/test/1 72 ms
09:41:11,100 DEBUG [VirtualHostFilter:40] [http-8080-2]=< com.liferay.portal.servlet.filters.virtualhost.VirtualHostFilter /user/test/1 75 ms
09:41:11,100 DEBUG [SessionIdFilter:40] [http-8080-2]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /user/test/1 80 ms

You're wondering: "Great! But what does this all mean?"

So, first off, filters are stacked right? So, each filter step starts with output like so:

09:40:43,565 DEBUG [SessionIdFilter:40] [http-8080-2]>

Notice that there is a ">" to indicate that we're going into this filter, namely "SessionIdFilter". The thread handling this particular request is "http-8080-2". With each subsequent filter we increase in depth along the processing chain. So each "=" indicates that we're inside one more filter. "=======>" means we're 8 steps into the filter chain.

Now, any line with "<" means we're on our way back and will also include the time spent during this filter's life for this request.

09:41:11,098 DEBUG [StripFilter:40] [http-8080-2]========< com.liferay.portal.servlet.filters.strip.StripFilter /user/test/1 54 ms

So let's refine this information even more using a command to list only the total times for all requests.

$ grep "\]<" catalina.out.log
09:41:04,716 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /user/test/1 9305 ms
09:41:05,000 DEBUG [SessionIdFilter:40] [http-8080-2]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/js/everything.js 222 ms
09:41:05,090 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/js/liferay/portlet_css.js 48 ms
09:41:11,100 DEBUG [SessionIdFilter:40] [http-8080-2]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /user/test/1 80 ms
09:41:11,944 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /web/182782/1 842 ms
09:41:15,633 DEBUG [SessionIdFilter:40] [http-8080-2]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /web/182786/1 1013 ms
09:41:15,742 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/portlet/rss/css.jsp 165 ms
09:41:15,882 DEBUG [SessionIdFilter:40] [http-8080-2]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/themes/classic/images/common/add-page.png 3 ms
09:41:16,016 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /c 14 ms
09:41:16,278 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /web/182786/1 257 ms
09:41:20,568 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /c/portal/render_portlet 4241 ms
09:42:05,443 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /web/182786/1 3051 ms
09:42:05,648 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /html/themes/classic/images/common/add-page.png 2 ms
09:42:05,666 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /c 8 ms
09:42:05,834 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /web/182786/1 159 ms
09:42:13,619 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /web/182786/1 137 ms
09:42:13,949 DEBUG [SessionIdFilter:40] [http-8080-3]< com.liferay.portal.servlet.filters.sessionid.SessionIdFilter /user/test/1 324 ms

Well, there you have it. Another of Liferay's hidden gems disclosed.

Oh yeah, output of this configuration is very verbose, so you probably don't want to turn this on in production unless you know your logs can handle it. Also, this alone will likely cost some processing time as the log is under contention by concurent threads. And while this is likely the case with any kind of monitoring tool, it may slightly adversely affect the actual times from when logging is optimized for a production environment (like only logging ERROR and worse messages). On the other hand it provides pretty deep insight into the portal's processing time expenditures and really should not incur much cost when the server is under relatively light load (like in a dev environment).

Also note that the times above were taken with a mostly cold portal, fresh after a deploy, which means that all the jsps still weren't compiled, etc. Your best and most accurate results will come with a fairly warm server once memory is primed and caches are seeded.

Velocity Improvements

Staff Blogs 9 de Janeiro de 2009 Por Ray Augé Staff

So I finally got around to making the Velocity improvements I'd had planned on doing for the last several months. We completely removed the Velocity singleton usage and replaced it with the VelocityEngine instance design. With this, Liferay is able to play nicely with other velocity apps in the same JVM. Also, because we centralized the usage, we were able to optimize tool loading so that we don't waste resources repopulating those all the time. This also allowed us to pre-configure different tool configurations to control accessibly for the purpose of security. i.e. different tools from Themes than from Web Content Templates.

Since much of our Velocity use originates as VTL in String form, we put in place Velocity's new StringResourceLoader so that we could make sure to obtain and cache the resulting Template objects by name, such that we can gain from not having our Strings re-parsed on each evaluation.

For instance, each layout template is provided to the Velocity engine as a VTL String which is evaluated into a Template object and then merged with the parameters put into the velocity context. The optimization comes from first providing a name which uniquely idientifes the VTL String, which results in a Template object that gets cached and which we can later call by name. As such, we don't have to pay the cost of re-evaluating the VTL String each time as before. If you get 100 requests per minute, it means 100 evaluations of the VTL String to Template and then 100 merges of Template and context parameters. And yet the VTL String does not change so often, so retaining the resulting Template is optimal, as that is the most costly step in the process.

Considering how many VTL Strings the portal is handling on a given request: theme templates, layout templates, journal templates, you can see how this can begin to pile up. A cached Template object can be used over and over, with very little cost for the merge phase. This might seem like it means higher memory consumption due to the fact that we're hanging onto these Template objects, but in fact this is not the case, since the Template objects can be used for many requests and for as long as we're willing to keep them around. We're actually drastically reducing object creation and subsequent collection.

Hopefully the new design will prove to be a significant performance improvement and pay off in the form of a more responsive user experience, higher overall throughput, and finally greater concurrent load due to more efficient resource utilization.

Note: This change was committed to trunk today, and was backported to the 5.1.x branch earlier this evening.

Enjoy!

Portal Hook Plugins

Staff Blogs 7 de Outubro de 2008 Por Ray Augé Staff

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.

The New Liferay Permission Algorithm (a.k.a. 5, a.k.a RBAC)

Staff Blogs 23 de Setembro de 2008 Por Ray Augé Staff

Update (Feb 1, 2011): The default permission algorithm from version 6.0+ is now 6. It uses a fast bitmask persistence implementation of RBAC and is functionally equivalent to 5. However, there is only a single table for permissions, and a single table row per resource, per role, and so the data size is significantly smaller (probably at least 1/5 of previous). There is an built-in, automated migration tool to go from 1-5 to 6. There aren't any valid reason to not migrate from 5 to 6.

 

Well, it's been almost 2 months since we introduced a new permission checking algorithm into the portal. The key features of the new algorithm are:

  1. increased speed of evaluation
  2. simpler usage
  3. increased performance
  4. increased speed of evaluation
  5. and lastly increased speed of evaluation and increased performance

So, how did we acheive this and what did we have to sacrific to get it?

With the old default algorithm (a.k.a. 2) we had all kinds of objects to which we could assign permissions; users, groups, roles, orgs, user groups. While this sounds great, it really isn't for several reasons.

1) Having this many objects to which we can assign permissions means that evaluating whether a user has a particular permission on some entity incurs a check on all those objects. Not to mention the fact that some of those objects support inheritence. This lead to some very complex and expensive JOIN queries.

2) The simple fact that EVERY User could have permissions on a given entity meant that we had to define defaults on these entities. This would lead to situations where a user would visit a portal page which contained entities they had never before encountered and suddenly there would be a tone of DB interactions to create these default permissions for these new objects. Imagine a Message Board page with 20 posts happening over night, morning comes and traffic increases to say 100 users (user who had not encountered the 20 posts before) per minute. That is 100 * 20 / minute new objects being created. This lead to hundreds of DB interactions per second on some highly dynamic sites.

3) Managing permissions on so many different objects was difficult at best, and utterly confusing a worst.

The solution

What we did was implement a system based on the Roles Based Access Control (RBAC) paradigm. We had the foundation for such a system in place, we simply had to reduce the number of objects to which we could assign permissions to only one; Role. This allowed us to perform shorter, faster queries at evaluation time with many fewer JOINS. Also, since we eliminated the assignment of Permissions to User objects we no longer had to create defaults for Users encountering new entities. This increased the concurrent load the portal could handle by a huge factor.

For a short while after the initial system was in place we realized that we had overlooked one key issue, "ownership".

Because User is not assigned permissions, how can we define the permissions granted to the original creator? Well, it took a while to discover a flexible enough solution that would not lead us back down a patch which would cause us to lose our recent performance increase. We definitely did not want to go back to one to one association of permissions to Users.

The solution came in the form of an "implied Role". The "implied" meaning that this Role, though it is a Role like any other, can't be assigned to anything, can't be assigned too, rather it is the result of "state", when a new object is created the default permissions normally associated with the User object are associated with the implied "Owner" Role. Then, on objects which are "owned" (meaning they have a userId field, like a Message Board Message, Blog Entry, Bookmarks Entry, Journal Article, etc.) we first check if the current User is the owner of the object. If so, that user inherites the "Owner" Role. Now, since the "Owner" role is actually a real role, an administrator can manage permissions associated with the Owner Role for any object, by the normal means.

Other cases where we want to customize permissions specifically for a give user or set of users, or even with User Groups or Orgs, we can do this through Roles and then assign those. We lose non of the capability we had before, we increased the performance of the portal significantly, and also made permissions management far easer.

Also, since we only have one type of object on which we can assign permissions, it's easier to map onto external autorization systems, because most of those are already RBAC based, like LDAP.

Calling Liferay Services from XSL Journal Templates

Staff Blogs 23 de Setembro de 2008 Por Ray Augé Staff

Recently I was again asked how to call a more complex Liferay service using XSL.

Here is an example of getting CalEvents from XSL and printing the list. The first thing you might find is that the iteration is rather strange. That's because XSL has no notion of arrays or lists, other than nodelists that is. So we have to get around that by creating a template construct to represent our loop logic.

<?xml version="1.0"?>
<xsl:stylesheet 
	version="1.0" 
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
	xmlns:getterUtil="xalan://com.liferay.portal.kernel.util.GetterUtil"
	xmlns:java="http://xml.apache.org/xalan/java"
	xmlns:calEventLocalServiceUtil="xalan://com.liferay.portlet.calendar.service.CalEventLocalServiceUtil"
	xmlns:calFactoryUtil="xalan://com.liferay.portal.kernel.util.CalendarFactoryUtil"
	xmlns:userLocalServiceUtil="xalan://com.liferay.portal.service.UserLocalServiceUtil"
	exclude-result-prefixes="java" 
	extension-element-prefixes="getterUtil userLocalServiceUtil">

	<xsl:output method="html" omit-xml-declaration="yes"/>
	<xsl:param name="groupId" />
	<xsl:param name="locale" />
		
	<xsl:template match="/">

		<xsl:variable name="remote-user" select="/root/request/remote-user" />
		
		<xsl:text>Remote User: </xsl:text>
		<xsl:value-of select="$remote-user" />
		
		<div class="separator"><!--//--></div>
		
		<xsl:choose>
			<xsl:when test="$remote-user != ''">
				<xsl:variable name="user" select="userLocalServiceUtil:getUserById(getterUtil:getLong($remote-user))" />
				<xsl:variable name="timeZone" select="java:getTimeZone($user)" />
				<xsl:variable name="calendar" select="calFactoryUtil:getCalendar($timeZone, $locale)" />
				<xsl:variable name="events" select="calEventLocalServiceUtil:getEvents($groupId, $calendar)" />
	
				<xsl:value-of select="java:size($events)" />
				<xsl:text> events were found.</xsl:text>
				<br/><br/>

				<xsl:call-template name="for.loop">
					<xsl:with-param name="i" select="'0'" />
					<xsl:with-param name="count" select="java:size($events)" />
					<xsl:with-param name="list" select="$events" />
				</xsl:call-template>

			</xsl:when>
			<xsl:otherwise>
				<xsl:text>Hello! Please log in.</xsl:text>
			</xsl:otherwise>
		</xsl:choose>

	</xsl:template>

	<!-- This is what you customize to tailor your output per item. -->
	<xsl:template name="do.item">
		<xsl:param name="i" />

		<!-- 'item' is an object in the list -->
		<xsl:param name="item" />
		
		<xsl:value-of select="java:getTitle($item)" />
		<br/>
 	 </xsl:template>

	<!-- Don't touch bellow code. -->
	<xsl:template name="for.loop">
		<xsl:param name="i" />
		<xsl:param name="count" />
		<xsl:param name="list" />

		<xsl:if test="$i &lt; $count">
  	 	 	<xsl:call-template name="do.item">
 	 	 	 	<xsl:with-param name="i" select="$i" />
 	 	 	 	<xsl:with-param name="item" select="java:get($list, $i)" />
 	 	 	</xsl:call-template>
		</xsl:if>
 
 		<xsl:if test="$i &lt; $count">
  	 	 	<xsl:call-template name="for.loop">
 	 	 	 	<xsl:with-param name="i" select="$i + 1" />
  	 	 	 	<xsl:with-param name="count" select="$count" />
  	 	 	 	<xsl:with-param name="list" select="$list" />
 	 	 	</xsl:call-template>
 	 	 </xsl:if>
 	 </xsl:template>

</xsl:stylesheet>

There is no need to change the for.loop template. Only change the do.item template to operate on your list item. The for.loop can be re-used and in fact could be placed in a utility template and included rather than embedded.

Another thing you might ask is where did groupId and locale parameters come from. Well, those are two params automatically included in all our Journal XSL templates. You can just use them.

Journal VM Template meets SAXReaderUtil

Staff Blogs 11 de Setembro de 2008 Por Ray Augé Staff

Recently my friend Journal VM Template was feeling a little down in the dumps because of his lack of ability when it comes to handling XML content. He was feeling inferior to Journal XSL Template because of this one's superlative ability in dealing with any type of XML content, local and remote.

Not liking to see my friend down in the dumps, because he really has many other likeable features, I decided that it was time for an intervention. I got together with a new friend I'd recently started working with, SAXReaderUtil. SAXReaderUtil is a new and upcomming character around the Liferay codebase, and has tremendous skill when if comes to dealing with XML content.

He can easily get XML from Strings, Files, java.net.URLs, java.io.Readers, InputStreams, and most recently from a plain old String URLs. Not only that, but he can get you Documents with one line, can handle XPATH, do Node sorting, etc.. all in very little code.

Not only that, but all his buddies, Element, Node, and Branch (because he's a relative of dom4j) are all equally great to work with and very skilled.

Well, I'll show some code in a minute, but what I really wanted to tell you was that, I introduced SAXReaderUtil to Journal VM Template and they are getting along very well. So well, in fact, that I'm itching to show you some of the cool stuff these two can do now that they are working together.

Ok, so let's make a new Document from scratch:

#set ($document = $saxReaderUtil.read("<friends/>"))
#set ($root = $document.getRootElement())
#set ($friends = ["Journal VM Template", "SAXReaderUtil", "Document", "Element", "Branch", "Node"])
#foreach ($friend in $friends)
  #set ($friendEl = $root.addElement("friend"))
  #set ($friendEl = $friendEl.addText($friend))
#end

<pre>$htmlUtil.escape($document.asXML())</pre>

The output should look something like this (but not pretty printed, I did that):

<?xml version="1.0" encoding="UTF-8"?>
<friends>
  <friend>Journal VM Template</friend>
  <friend>SAXReaderUtil</friend>
  <friend>Document</friend>
  <friend>Element</friend>
  <friend>Branch</friend>
  <friend>Node</friend>
</friends>

Cool eh?

Ok, now suppose we mocked up a whole doc full of content and we want to provide it to some external client, an RSS reader, as a response to an AJAX call, etc.

First off, make sure that the article using the template is on a public page (so it's not gonna give you permission problems to start with). Next, lets add a little bit more code to make this content available as a regular old feed.

#set ($document = $saxReaderUtil.read("<friends/>"))
#set ($root = $document.getRootElement())
#set ($friends = ["Journal VM Template", "SAXReaderUtil", "Document", "Element", "Branch", "Node"])
#foreach ($friend in $friends)
  #set ($friendEl = $root.addElement("friend"))
  #set ($friendEl = $friendEl.addText($friend))
#end

#if ($request.window-state != "exclusive")
  <pre>$htmlUtil.escape($document.asXML())</pre>
  <a href="${request.render-url-exclusive}">FEED ME</a>
#else
$document.asXML()
#end

Now, click the "FEED ME" link and see what you get... (view source...)

That's right.. Whoop! Whoop!! Who's your Daddy???

That effectively turns your regular old templates into a relatively simple service end point. Tie that together with the ability to call Liferay's underlying services and you can publish any kind of feed you like... could even provide reports for some remote system which process some of your data blah blah... you get the idea.

But what if you want the template to be a consumer of some data in XML and produce a view of that data. A common scenario is a custom Journal Article list. Let's write some code to do that.

#set ($document = $saxReaderUtil.readURL("http://localhost:8080/lportal/c/journal/get_articles?groupId=14&delta=1"))

<textarea style="height: 800px; width: 500px;">
$document.asXML()
</textarea>

So, I'm making an API call to the backend Journal API to get the most recent article (delta=1) updated in the community with groupId=14. So first off, it's worth noting that it was easy to make the call, we just used the readURL method. Next, we're just dumping the contents into a textarea. This is the first step to see if we have good data to work with. It should look something like this.

Ok, so we were talking about our new friend SAXReaderUtil. It seems he's doing a fine job so far. Let's see what other tricks him and his buddies provide.

Let's get the list of articles using XPATH (of course we have one... but you'd likely have more right?).

#set ($document = $saxReaderUtil.readURL("http://localhost:8080/lportal/c/journal/get_articles?groupId=14&delta=1"))
#set ($root = $document.getRootElement())
#set ($articles = $root.selectNodes("/result-set/result/root"))

<textarea style="height: 800px; width: 500px;">
$articles.get(0).asXML()
</textarea>

Now we know our list is right, we can iterate through each article and get some details about each one.

#set ($document = $saxReaderUtil.readURL("http://localhost:8080/lportal/c/journal/get_articles?groupId=14&delta=1"))
#set ($root = $document.getRootElement())
#set ($articles = $root.selectNodes("/result-set/result/root"))

<ul>
#foreach ($article IN $articles)
#set ($articleId = $article.selectSingleNode("dynamic-element[@name='reserved-article-id']/dynamic-content"))
#set ($articleTitle = $article.selectSingleNode("dynamic-element[@name='reserved-article-title']/dynamic-content"))
#set ($articleModifiedDate = $article.selectSingleNode("dynamic-element[@name='reserved-article-modified-date']/dynamic-content"))
#set ($articleAuthorName = $article.selectSingleNode("dynamic-element[@name='reserved-article-author-name']/dynamic-content"))
<li>
<a href="${request.render-url-maximized}${request.portlet-namespace}articleId=${articleId.text}">${articleTitle.text}</a>
<br/>
<span style="font-size: smaller;">${articleAuthorName.text}, ${articleModifiedDate.text}</span>
#end
</ul>

What we get is this:

Ok, we're only showing one... how does it look with more. All we do is modify the API query by setting the delta parameter to something higher. Let's pick 4.

Now notice that the URL's actually work, placing the given article in Maximized mode. You can choose to not go maximized if you like (use ${request.render-url} instead of ${request.render-url-maximized}. And if you use a friendly URL you can target another portlet on the page completely, using the traditional "narrow-side-nav-links vs. wide-side-view-port" model.

Anyway, It's pretty impressive what you can accomplish when friends work together. I'd suggest visiting with Journal VM Template and SAXReaderUtil & Co. as soon as you can take Liferay 5.1.2 for a spin (5.1.x until the official release). All my friends are enjoying their new acquaintances. I hope you enjoy them too.

Calling Java (tools|services) from XSL Journal Templates

Staff Blogs 3 de Setembro de 2008 Por Ray Augé Staff

Recently a coleague asked if it was possible to call java from XSL.

It is possible, and here is the example I provided.

<?xml version="1.0"?>
<xsl:stylesheet 
	version="1.0" 
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
	xmlns:getterUtil="xalan://com.liferay.portal.kernel.util.GetterUtil"
	xmlns:java="http://xml.apache.org/xalan/java"
	xmlns:userLocalServiceUtil="xalan://com.liferay.portal.service.UserLocalServiceUtil"
	exclude-result-prefixes="java" 
	extension-element-prefixes="getterUtil userLocalServiceUtil">

	<xsl:output method="html" omit-xml-declaration="yes"/>
		
	<xsl:template match="/">

		<xsl:variable name="remote-user" select="/root/request/remote-user" />
		
		<xsl:text>Remote User: </xsl:text>
		<xsl:value-of select="$remote-user" />
		
		<div class="separator"><!--//--></div>
		
		<xsl:choose>
			<xsl:when test="$remote-user != ''">
				<xsl:variable name="user" select="userLocalServiceUtil:getUserById(getterUtil:getLong($remote-user))" />
	
				<xsl:text>Hello </xsl:text>
				<xsl:value-of select="java:getFullName($user)" />
				<xsl:text>!</xsl:text>
			</xsl:when>
			<xsl:otherwise>
				<xsl:text>Hello! Please log in.</xsl:text>
			</xsl:otherwise>
		</xsl:choose>

	</xsl:template>
</xsl:stylesheet>
VM (VTL) is great and all, but when it comes to dealing with XML data, you simply can't beat the power of XSL. Add to that the power of inline java and you can do some nifty stuff.

Liferay Portlets as Standalone Desktop Apps??? OOTB

Staff Blogs 28 de Agosto de 2008 Por Ray Augé Staff

In Liferay we have this cool new feature to run portlets as embedded widgets from any webpage anywhere.

Tired of loosing you chat sessions on every page refresh???

No more!!!

Well feast your eyes on Liferay Portls running as APPS on your desktop. Completely standalone, no shared memory, if one process crashes the others will not.

Take a look at these babies.

 

Chat Portlet:

 

Message Boards:

Message Boards

 

Zimbra eat your heart out... Every portlet build on Liferay automatically becomes your latest Desktop App using none other than the Prism Mozilla project.

Check it out at http://wiki.mozilla.org/WebRunner.

All you need is to grab the Shared "widget URL" we generate for your portlet and setup a Prism link, done!

 

 

Is your laptop battery this dead?

Staff Blogs 30 de Julho de 2008 Por Ray Augé Staff

Sample scripts for the Ruby Console portlet [1]

Staff Blogs 9 de Julho de 2008 Por Ray Augé Staff

Here is a sample script for the Ruby Console portlet to test various aspects of the Dynamic Query API.

import com.liferay.portal.kernel.util.Time
import java.lang.System
import java.util.Date

module Orm
   include_package "com.liferay.portal.kernel.dao.orm"
end
module Model
   include_package "com.liferay.portal.model"
end
module Service
   include_package "com.liferay.portal.service"
end

$resourceResponse.setContentType "text/html"

out = $resourceResponse.getPortletOutputStream

date24HoursAgo = Date.new(System.currentTimeMillis - Time::HOUR * 24)

dq1 = Orm::DynamicQueryFactoryUtil.forClass(Model::User.java_class)

pl1 = Orm::ProjectionFactoryUtil.projectionList
pl1.add(Orm::ProjectionFactoryUtil.property("emailAddress"))
pl1.add(Orm::ProjectionFactoryUtil.property("lastLoginDate"))
pl1.add(Orm::ProjectionFactoryUtil.property("lastLoginIP"))

dq1.setProjection(pl1)
dq1.add(Orm::PropertyFactoryUtil.forName("lastLoginDate").gt(date24HoursAgo))
dq1.addOrder(Orm::OrderFactoryUtil.desc("emailAddress"))

result = Service::UserLocalServiceUtil.dynamicQuery(dq1, 0, 20)

out.println result.size
out.println "<br/>"
out.println "<table width='100%' border='1'>"
result.each do |row|
  out.println "<tr>"
  row.each do |field|
    out.println "<td>#{field}</td>"
  end
  out.println "</tr>"
end
out.println "</table>"

[HOWTO] Personalization - Getting current user attributes

Staff Blogs 16 de Maio de 2008 Por Ray Augé Staff

Portals are all about personalization, and Liferay is no different.

I mean, there are many, many other things involved in portals other than personalization. But none of them really make sense without it.

Personalization is the dressing that makes your portal salad bearable. So, it has to be pretty good dressing.

Beyond the portal and personalization, we have the reason the portal exists at all: Portlets. Portlets are the ingredients needed to make a portal salad worth eating.

  1. You have your greens (the hard working portlets: E-Commerce, BI, BPM, CMS, etc.).
  2. Then you have your non-green veggies (Email, Forums, Blogs, Wiki, etc.).
  3. Finally you have your crutons and bacon bits, the stuff we know we can live without but just can't bring ourselves to leave out (social networking and other generally superfluous gadgets, widgetry, and such).

Yet it doesn't end there, it's gotta be MY salad, not someone else's... and it's gotta have MY name on it too. Who wants to eat someone else's food anyway...

That said, personalization at the portlet level is very important. There are two types of portlet personalization mechanisms available in Liferay.

  1. Personalization defined by the Portlet spec(s) (low fat, low cal. dressing)
  2. Liferay personalization (fatty, rich, tasty dressing)

Let's go through putting both to use in your Portlet application and then you can decide for yourself which best suites your diet.

Personalizaton via Portlet spec(s)

Both the 1.0 version and 2.0 (upcomming) versions of the spec define a mechanism called "user-attributes" defined as such:

"The deployment descriptor of a portlet application must define the user attribute names the portlets use. The following example shows a section of a deployment descriptor defining a few user attributes:

<portlet-app>
	...
	<user-attribute>
		<description>User Given Name</description>
		<name>user.name.given</name>
	</user-attribute>
	<user-attribute>
		<description>User Last Name</description>
		<name>user.name.family</name>
	</user-attribute>
	<user-attribute>
		<description>User eMail</description>
		<name>user.home-info.online.email</name>
	</user-attribute>
	<user-attribute>
		<description>Company Organization</description>
		<name>user.business-info.postal.organization</name>
	</user-attribute>
	...
<portlet-app>
A deployer must map the portlet application’s logical user attributes to the corresponding user attributes offered by the runtime environment. At runtime, the portlet container uses this mapping to expose user attributes to the portlets of the portlet application. User attributes of the runtime environment not mapped as part of the deployment process must not be exposed to portlets."

[JavaTM Portlet Specification, version 1.0, PLT.17.1]

This means that without having to do anything other than configuration, the portlet is requesting access to the defined user attributes. The list of available attributes can be found in the spec docs.

The next part is of course getting access to those user attributes at runtime.

Here is a complete JSP which handles getting the user.name.given attribute.

<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>

<%@ page import="javax.portlet.PortletRequest"%>
<%@ page import="java.util.Map"%>

<portlet:defineObjects />

<%
Map userInfo = (Map)renderRequest.getAttribute(PortletRequest.USER_INFO);
String givenName = (userInfo != null) ? (String)userInfo.get("user.name.given") : "";
%>

Given Name: <%= givenName %>

The result looks something like the following:

[Image 1]

That's 100% spec complient and includes the following list of attributes:

package com.liferay.portlet;

public class UserAttributes {

	// Mandatory Liferay attributes

	public static final String LIFERAY_COMPANY_ID = "liferay.company.id";

	public static final String LIFERAY_USER_ID = "liferay.user.id";

	public static final String USER_NAME_FULL = "user.name.full";

	// See page 119 of the JSR 168 spec

	public static final String USER_BDATE = "user.bdate";

	public static final String USER_GENDER = "user.gender";

	public static final String USER_EMPLOYER = "user.employer";

	public static final String USER_DEPARTMENT = "user.department";

	public static final String USER_JOBTITLE = "user.jobtitle";

	public static final String USER_NAME_PREFIX = "user.name.prefix";

	public static final String USER_NAME_GIVEN = "user.name.given";

	public static final String USER_NAME_FAMILY = "user.name.family";

	public static final String USER_NAME_MIDDLE = "user.name.middle";

	public static final String USER_NAME_SUFFIX = "user.name.suffix";

	public static final String USER_NAME_NICKNAME = "user.name.nickName";

	public static final String USER_HOME_INFO_POSTAL_NAME = "user.home-info.postal.name";

	public static final String USER_HOME_INFO_POSTAL_STREET = "user.home-info.postal.street";

	public static final String USER_HOME_INFO_POSTAL_CITY = "user.home-info.postal.city";

	public static final String USER_HOME_INFO_POSTAL_STATEPROV = "user.home-info.postal.stateprov";

	public static final String USER_HOME_INFO_POSTAL_POSTALCODE = "user.home-info.postal.postalcode";

	public static final String USER_HOME_INFO_POSTAL_COUNTRY = "user.home-info.postal.country";

	public static final String USER_HOME_INFO_POSTAL_ORGANIZATION = "user.home-info.postal.organization";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_INTCODE = "user.home-info.telecom.telephone.intcode";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_LOCCODE = "user.home-info.telecom.telephone.loccode";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_NUMBER = "user.home-info.telecom.telephone.number";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_EXT = "user.home-info.telecom.telephone.ext";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_COMMENT = "user.home-info.telecom.telephone.comment";

	public static final String USER_HOME_INFO_TELECOM_FAX_INTCODE = "user.home-info.telecom.fax.intcode";

	public static final String USER_HOME_INFO_TELECOM_FAX_LOCCODE = "user.home-info.telecom.fax.loccode";

	public static final String USER_HOME_INFO_TELECOM_FAX_NUMBER = "user.home-info.telecom.fax.number";

	public static final String USER_HOME_INFO_TELECOM_FAX_EXT = "user.home-info.telecom.fax.ext";

	public static final String USER_HOME_INFO_TELECOM_FAX_COMMENT = "user.home-info.telecom.fax.comment";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_INTCODE = "user.home-info.telecom.mobile.intcode";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_LOCCODE = "user.home-info.telecom.mobile.loccode";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_NUMBER = "user.home-info.telecom.mobile.number";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_EXT = "user.home-info.telecom.mobile.ext";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_COMMENT = "user.home-info.telecom.mobile.comment";

	public static final String USER_HOME_INFO_TELECOM_PAGER_INTCODE = "user.home-info.telecom.pager.intcode";

	public static final String USER_HOME_INFO_TELECOM_PAGER_LOCCODE = "user.home-info.telecom.pager.loccode";

	public static final String USER_HOME_INFO_TELECOM_PAGER_NUMBER = "user.home-info.telecom.pager.number";

	public static final String USER_HOME_INFO_TELECOM_PAGER_EXT = "user.home-info.telecom.pager.ext";

	public static final String USER_HOME_INFO_TELECOM_PAGER_COMMENT = "user.home-info.telecom.pager.comment";

	public static final String USER_HOME_INFO_ONLINE_EMAIL = "user.home-info.online.email";

	public static final String USER_HOME_INFO_ONLINE_URI = "user.home-info.online.uri";

	public static final String USER_BUSINESS_INFO_POSTAL_NAME = "user.business-info.postal.name";

	public static final String USER_BUSINESS_INFO_POSTAL_STREET = "user.business-info.postal.street";

	public static final String USER_BUSINESS_INFO_POSTAL_CITY = "user.business-info.postal.city";

	public static final String USER_BUSINESS_INFO_POSTAL_STATEPROV = "user.business-info.postal.stateprov";

	public static final String USER_BUSINESS_INFO_POSTAL_POSTALCODE = "user.business-info.postal.postalcode";

	public static final String USER_BUSINESS_INFO_POSTAL_COUNTRY = "user.business-info.postal.country";

	public static final String USER_BUSINESS_INFO_POSTAL_ORGANIZATION = "user.business-info.postal.organization";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_INTCODE = "user.business-info.telecom.telephone.intcode";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_LOCCODE = "user.business-info.telecom.telephone.loccode";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_NUMBER = "user.business-info.telecom.telephone.number";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_EXT = "user.business-info.telecom.telephone.ext";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_COMMENT = "user.business-info.telecom.telephone.comment";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_INTCODE = "user.business-info.telecom.fax.intcode";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_LOCCODE = "user.business-info.telecom.fax.loccode";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_NUMBER = "user.business-info.telecom.fax.number";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_EXT = "user.business-info.telecom.fax.ext";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_COMMENT = "user.business-info.telecom.fax.comment";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_INTCODE = "user.business-info.telecom.mobile.intcode";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_LOCCODE = "user.business-info.telecom.mobile.loccode";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_NUMBER = "user.business-info.telecom.mobile.number";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_EXT = "user.business-info.telecom.mobile.ext";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_COMMENT = "user.business-info.telecom.mobile.comment";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_INTCODE = "user.business-info.telecom.pager.intcode";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_LOCCODE = "user.business-info.telecom.pager.loccode";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_NUMBER = "user.business-info.telecom.pager.number";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_EXT = "user.business-info.telecom.pager.ext";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_COMMENT = "user.business-info.telecom.pager.comment";

	public static final String USER_BUSINESS_INFO_ONLINE_EMAIL = "user.business-info.online.email";

	public static final String USER_BUSINESS_INFO_ONLINE_URI = "user.business-info.online.uri";

}

It should be noted that as of right now, we haven't implemented the additonal attributes defined in javax.portlet.PortletRequest.P3PUserInfos, of the current version 2.0 recommendation.



Liferay Personalizaton

Liferay personalization provides access to pojos which can be interacted with in a much richer fashion than above. It also ties your application to Liferay so, if you plan to target more than just Liferay as plaform, don't use it.

The easiest way to take avantage of Liferay personalization is to use a couple of Liferay's taglibs; more precisely, the liferay-theme and liferay-ui taglibs.

Since these are not included in a deployed portlet by default, add the following property to your liferay-plugin-package.properties file:

portal.dependency.tlds=\
    liferay-ui.tld,\
    liferay-theme.tld

and the following taglib definitions to your web.xml:

	<taglib>
		<taglib-uri>http://liferay.com/tld/theme</taglib-uri>
		<taglib-location>/WEB-INF/tld/liferay-theme.tld</taglib-location>
	</taglib>
	<taglib>
		<taglib-uri>http://liferay.com/tld/ui</taglib-uri>
		<taglib-location>/WEB-INF/tld/liferay-ui.tld</taglib-location>
	</taglib>

Now that we have the right tools available, let's start with the very simplest example that gives us the most bang.

<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme" %>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui" %>

<liferay-theme:defineObjects />
<portlet:defineObjects />

<liferay-ui:user-display userId="<%= user.getUserId() %>" />

The result of the above JSP code is something like the following:

[Image 2]

Now you might be asking yourself "Where did that user object suddenly come from?"

The answer is simple! It comes from the <liferay-theme:defineObjects /> tag we used. This tag puts a whole bunch of context sensitive objects into our JSP page context, including the com.liferay.portal.model.User pojo associated with the current user..

The objects that are injected into the pageContext by the <liferay-theme:defineObjects /> tag are:

  • themeDisplay - com.liferay.portal.theme.ThemeDisplay
  • company - com.liferay.portal.model.Company
  • account - com.liferay.portal.model.Account (deprecated)
  • user - com.liferay.portal.model.User
  • realUser - com.liferay.portal.model.User
  • contact - com.liferay.portal.model.Contact
  • ?layout - com.liferay.portal.model.Layout
  • ?layouts - List<com.liferay.portal.model.Layout>
  • plid - java.lang.Long
  • ?layoutTypePortlet - com.liferay.portal.model.LayoutTypePortlet
  • portletGroupId - java.lang.Long
  • permissionChecker - com.liferay.portal.security.permission.PermissionChecker
  • locale - java.util.Locale
  • timeZone - java.util.TimeZone
  • theme - com.liferay.portal.model.Theme
  • colorScheme - com.liferay.portal.model.ColorScheme
  • portletDisplay - com.liferay.portal.theme.PortletDisplay

Wow, now that's lots of bacon!!! I like bacon!

I won't try to explain the use of all of these objects here on this post. Suffice it to say that after exploring all of these objects and the remaining tags available to you in the Liferay taglibs, you shouldn't run out of personalization options any time soon.

You can't use JSP taglibs?

Never fear! All these objects are actually obtained by various getter methods on the com.liferay.portal.theme.ThemeDisplay object. We just put them right into the page context to cut down on code.

So, all you need is the themeDisplay object, and you can get it from the portletRequest like so:

ThemeDisplay themeDisplay = (ThemeDisplay)req.getAttribute(WebKeys.THEME_DISPLAY);

themeDisplay.getCompany();
themeDisplay.getAccount();
themeDisplay.getUser();
themeDisplay.getRealUser();
themeDisplay.getContact();

if (themeDisplay.getLayout() != null) {
	themeDisplay.getLayout();
}

if (themeDisplay.getLayouts() != null) {
	themeDisplay.getLayouts();
}

themeDisplay.getPlid());

if (themeDisplay.getLayoutTypePortlet() != null) {
	themeDisplay.getLayoutTypePortlet();
}

new Long(themeDisplay.getPortletGroupId());
themeDisplay.getPermissionChecker();
themeDisplay.getLocale();
themeDisplay.getTimeZone();
themeDisplay.getTheme();
themeDisplay.getColorScheme();
themeDisplay.getPortletDisplay();

Well, feel to ask questions about the ones for which the meanings are less than obvious. The Message Boards are also full of answers regarding various personalization topics. And remember to have a look at the API docs on the site for the methods provided from each of these objects.

[HOWTO] Dowloading files from a Portlet

Staff Blogs 14 de Maio de 2008 Por Ray Augé Staff

Many people ask how to achieve file downloads from a portlet project.

I'm going to try putting this mystery to rest by first explaining the limitations and then showing some old and new ways of overcoming them in Liferay. To be clear on terminology we'll call anything, other than the usual presentation content, that we want to deliver to the user a resource. Examples of resources are: images, binary documents, css stylesheets, js files, and typically things that we want to dynamically generate access to users to download. They are also often generated on the fly, like a PDF report, or a captcha image.

To begin, the JSR-168/Portlet 1.0 spec did not have support for delivering anything other than a blob of text/html content. Nor did it support delivering such content outside of a response which included the portal's wrappings.

Here is a brief (and not all encompassing) recent history of resource downloads in Liferay.

Liferay <= 4.1.x: At this point the ONLY ways to provide download links were:

  1. provide a direct link to the resource
  2. delegate the functionality to a servlet

In Liferay's core this was very simple because we use Struts, so creating an action (and mapping) of type org.apache.struts.action.Action was relatively painless and we could quickly develop any download mechanism we needed. The only issue was that the URL could not be generated using the <portlet /> tag. A developer had to be aware of the path associated with the Action in order to access it.

For example, consider the following action-mapping from version 4.3.x's Document Library portlet:

<action path="/document_library/get_file" type="com.liferay.portlet.documentlibrary.action.GetFileAction" />

If you look at the code for com.liferay.portlet.documentlibrary.action.GetFileAction you will see that it implements a method:

public ActionForward strutsExecute(
		ActionMapping mapping, ActionForm form, HttpServletRequest req, HttpServletResponse res)
	throws Exception;

This method, though found in com.liferay.portal.struts.PortletAction, is simply a helper method which makes the PortletAction behave as a servlet by calling super.execute(mapping, form, req, res) directly.

In order to put this method to work we create an url as follows:

<a href="<%= themeDisplay.getPathMain() %>/document_library/get_file?folderId=<%= folderId %>&name=<%= HttpUtil.encodeURL(name) %>">
	<%= fileEntry.getTitle() %>
</a>

Notice that we haven't used any tag to generate the URL and we needed to include the portals context.

So, at this point if you were creating a portlet to be JSR-168 complient (outside of ext, or core), you could create an URL to a static resource like so (JSP example):

<a href="<%= renderRequest.getContextPath() %>/static_path/file_name.pdf">
	file_name.pdf
</a>

To create an URL to a dynamic resource, you would have to delegate to some servlet, like so:

<a href="<%= renderRequest.getContextPath() %>/servlet_path?p1=va&p2=v2">
	<%= someLinkText %>
</a>


Liferay <= 4.4.x and > 4.1.x: At this point we started to experiment with delivering portlet resources in a way similar to what was proposed in the upcoming JSR-286/Portlet 2.0 spec.

We had at our disposal a feature we had created which allowed us to deliver resources from portlets. It was used by specifying LiferayWindowState.EXCLUSIVE on a portlet URL. If using it from a actionURL it could deliver binary resources.

Example of returning a captcha image, from 4.3.x:

<portlet:actionURL windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>" var="captchaURL">
	<portlet:param name="struts_action" value="/message_boards/captcha" />
</portlet:actionURL>

On a renderURL it could be used for handling things like Ajax requests.

<form action="<liferay-portlet:renderURL 
			windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>">
		<portlet:param name="struts_action" value="/password_generator/view" />
	</liferay-portlet:renderURL>" 
	method="post" 
	name="<portlet:namespace />fm" 
	onSubmit="AjaxUtil.submit(this, {update: this.parentNode}); return false;">

Unfortunately, this feature was only usable from within the EXT environment or from within the core for handling binary resources, because final handling of the response required accessing the original HttpServletResponse.

Here is the response handling side of the capcha example:

public void processAction(
		ActionMapping mapping, ActionForm form, PortletConfig config,
		ActionRequest req, ActionResponse res)
	throws Exception {

	try {
		PortletSession ses = req.getPortletSession();

		String captchaText = _producer.createText();

		ses.setAttribute(WebKeys.CAPTCHA_TEXT, captchaText);

		HttpServletResponse httpRes =
			((ActionResponseImpl)res).getHttpServletResponse();

		_producer.createImage(httpRes.getOutputStream(), captchaText);

		setForward(req, ActionConstants.COMMON_NULL);
	}
	catch (Exception e) {
		_log.error(e);
	}
}


Solution for external portlets: In order to provide a feature which was usable from portlets outside of EXT & core, we created a interface available in portal-kernel.jar and made RenderResponseImpl implement this interface. The interface provide the following methods:

public interface LiferayRenderResponse extends RenderResponse {

	public void addDateHeader(String name, long date);

	public void addHeader(String name, String value);

	public void addIntHeader(String name, int value);

	public void setDateHeader(String name, long date);

	public void setHeader(String name, String value);

	public void setIntHeader(String name, int value);

	public void setResourceName(String resourceName);

}

Additionally, we added support for changing the content type returned from RenderResponse, but ONLY if the WinowState was equal to LiferayWindowState.EXCLUISVE.

Here is an example which returns a png image:

public void doView(RenderRequest req, RenderResponse res)
	throws IOException, PortletException {

	boolean logo = ParamUtil.getBoolean(req, "logo");

	if (logo && req.getWindowState().equals(LiferayWindowState.EXCLUSIVE)) {
		LiferayRenderResponse liferayRes = (LiferayRenderResponse)res;

		liferayRes.setContentType("image/png");
		liferayRes.addHeader(
			HttpHeaders.CACHE_CONTROL, "max-age=3600, must-revalidate");

		OutputStream out = liferayRes.getPortletOutputStream();

		InputStream in = // some InputStream

		if (in == null) {
			out.close();
		}
		else {
			byte[] buffer = new byte[4096];
			int len;

			while ((len = in.read(buffer)) != -1) {
				out.write(buffer, 0, len);
			}

			out.flush();
			in.close();
			out.close();
		}
	}
	else {
		include(viewJSP, req, res);
	}
}

Notice that you can specify the contentType, set headers, and write binary/text data directly to the portlet's OutputStream. One note is that the content type MUST be set before attempting to call liferayRes.getPortletOutputStream(); otherwise an exception will be raised.

And the url looks like this:

<img src="<portlet:renderURL
			portletMode="view"
			windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>">
		<portlet:param name="logo" value="true" />
	</portlet:renderURL>"
	alt="Image returned by portlet." />

This was pretty handy for everything from XML and JSON AJAX data, to dynamically generated binary resources.



Liferay >= 5.0.0: In the new release of Liferay, while still supporting all of the methods described above, we also support the brand new (and as of yet still unofficial) JSR-286/Portlet 2.0 feature ResourceRequest which is specifically designed for handling any resource delivery purely as a function of a portal & portlet which are JSR-286 compliant. It can equally well handle everything from binary files to JSON data returned by an AJAX request.

An example usage looks like this:

public void serveResource(ResourceRequest req, ResourceResponse res)
		throws PortletException, IOException {

	boolean logo = ParamUtil.getBoolean(req, "logo");

	if (logo) {
		res.setContentType("image/png");
		res.addProperty(
			HttpHeaders.CACHE_CONTROL, "max-age=3600, must-revalidate");

		OutputStream out = res.getPortletOutputStream();

		InputStream in = // some InputStream

		if (in == null) {
			out.close();
		}
		else {
			byte[] buffer = new byte[4096];
			int len;

			while ((len = in.read(buffer)) != -1) {
				out.write(buffer, 0, len);
			}

			out.flush();
			in.close();
			out.close();
		}
	}
}

And the URL looks like this:

<img src="<portlet:resourceURL>
		<portlet:param name="logo" value="true" />
	</portlet:resourceURL>"
	alt="Image returned by portlet." />

Well, there you have it.

[Tip] EhCache and Ubuntu (Hardy 8.04)

Staff Blogs 12 de Maio de 2008 Por Ray Augé Staff

If you are or you have tried installing a Liferay on a cluster of Ubuntu machines and are/were having problems with the ehCache, do this check:

]$ ping `hostname`

If it responds using an address on the loopback device, there's your problem.

By default, Ubuntu defines the machine's hostname on the loopback address (127.0.1.1), and since ehCache uses:

InetAddress.getLocalHost().getHostAddress()

to resolve the address used in association with the RMICachePeers it creates, these peers will have addresses of 127.0.1.1:port which will render them unreachable to other peers on the network.

Here is a successful test:

rotty@liferay-laptop:~$ ping `hostname`
PING liferay-laptop (192.168.0.243) 56(84) bytes of data.
64 bytes from liferay-laptop (192.168.0.243): icmp_seq=1 ttl=64 time=0.046 ms
64 bytes from liferay-laptop (192.168.0.243): icmp_seq=2 ttl=64 time=0.056 ms
64 bytes from liferay-laptop (192.168.0.243): icmp_seq=3 ttl=64 time=0.055 ms

Note that the address is one on the private subnet where the rest of the cluster nodes can see it.

FYI!

Expandos - What are they? And how do they help me? (Liferay Portal 5.0.1+)

Staff Blogs 24 de Abril de 2008 Por Ray Augé Staff

Updated: Wed May 28 10:53:09 EDT 2008.

See the follow-up article here.

In Javascript, "expando" means "to attach additional properties to an object".

That's a little bit of hint as to what Expandos are in Liferay.

In Liferay the Expando service is a "generic" service which allows you to dynamically define a collection of data. This data can be

  • typed (boolean, Date, double, int, long, short, String, and arrays of all those)
  • associated with a specific entity (e.g. 'com.liferay.portal.model.User')
  • arranged into any number of "columns"
  • available to plugins
  • accessed from Velocity templates
  • accessed via Liferay's JSON API through AJAX

 

The service also provides all the CRUD methods you typically need (Create/Retreive/Update/Delete).

You often need a way to add some custom field to Users or other portal entities and sometimes you need to do it fast and with minimal effort... it might even be temporary. Well, Expando is comming to the rescue.

To demonstrate how Expando works, I'll take you through a sample application which runs completely in as a velocity Journal Template.

So first off, lets get the required details out of the way.

Step 1) Create a Journal Structure. The structure is a requirement of any Journal Template because the connection between a Journal Article and a Journal Template is a function of the Journal Structure.

A very basic Structure is all we need in this case:

<root>
	<dynamic-element name='content' type='text'></dynamic-element>
</root>

Step 2) Create our Template

Our template "Language Type" will be VM (for Velocity), and we'll disable template caching by unckecking "Cacheable".

We'll start simple and work our way through the code. The first few lines will get some utility objects that we'll use later, and set a title for our app.

#set ($locale = $localeUtil.fromLanguageId($request.get("locale")))
#set ($dateFormatDateTime = $dateFormats.getDateTime($locale))

<h1>First Expando Bank</h1>

Step 3) We create our Article.

On the Article tab we essentially click "Add Article", give it a "Name", choose the Structure we created in Step 1) and click "Save".

The result should be something like this:

Now, the first task when using Expando is to define our table. Expando lets you do this programatically and with very little code.

Give our table a name.

...
<h1>First Expando Bank</h1>

#set ($accountsTableName = "AccountsTable")

Check to see if the table exists, and if not, create it.

...
#set ($accountsTableName = "AccountsTable")

#set ($accountsTable = $expandoTableLocalService.getTable($accountsTableName, $accountsTableName))

#if (!$accountsTable)
#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))
#end

We now have a table and we want to add columns to it. Since we're building a Bank app fields we need are firstName, lastName, and balance. We'll also keep track of the last time the account was updated, modifiedDate. The account number will be automitically generated and will represent the primary key for our table. In Expando, this primary key doesn't require a standalone column.

Also, we don't want this process to happen every time, so we'll only do it when the table is first created.

...
#if (!$accountsTable)
	#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))

	#set ($accountsTableId = $accountsTable.getTableId())

#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "firstName", 15)) ## STRING
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "lastName", 15)) ## STRING
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "balance", 5)) ## DOUBLE
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "modifiedDate", 3)) ## DATE
#end

Notice how we specified, for each column, an integer as the last parameter. These integer constants are defined in com.liferay.portlet.expando.model.ExpandoColumnConstants, and include the types I mentioned above.

Now that we have our table and columns setup, we want to add some logic that will detect and handle the various operations of our application. These are the CRUD operations we need for a complete app.

Let's start with some request handling and param setup.

...
#set ($renderUrl = $request.get("render-url"))
#set ($namespace = $request.get("portlet-namespace"))
#set ($cmd = $request.get("parameters").get("cmd"))

#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)

I won't go into much detail regarding the request handling abilitites of Journal Templates. Suffice it to say that it supports it. (I'll cover that in another Blog entry.)

Determine whether we have been passed an account number. As I mentioned earlier, Expando "records" have a primary key. That field is called "classPK", so we will keep that name to re-inforce the concept.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

Now we're going to check and see what operation (if any) we were asked to perform.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
...
#elseif ($cmd.equals("delete"))
...
#elseif ($cmd.equals("edit"))
...
#end

Adding/Updating an account is our first operation. In this case, we get the params from the request.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	#set ($firstName = $request.get("parameters").get("firstName"))
#set ($lastName = $request.get("parameters").get("lastName"))
#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
#set ($date = $dateTool.getDate())
#elseif ($cmd.equals("delete")) ... #elseif ($cmd.equals("edit")) ... #end

Do some form input checking (this one just does a basic check).

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	#set ($firstName = $request.get("parameters").get("firstName"))
	#set ($lastName = $request.get("parameters").get("lastName"))
	#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
	#set ($date = $dateTool.getDate())

	#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
...
#else
Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
#end
#elseif ($cmd.equals("delete")) ... #elseif ($cmd.equals("edit")) ... #end

So now, if we're ok, store the data.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	#set ($firstName = $request.get("parameters").get("firstName"))
	#set ($lastName = $request.get("parameters").get("lastName"))
	#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
	#set ($date = $dateTool.getDate())

	#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
		#if ($classPK <= 0)
#set ($classPK = $dateTool.getDate().getTime())
#end

#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))

#if ($cmd.equals("update"))
Thank you, ${firstName}, for updating your account with our bank!
#else
Thank you, ${firstName}, for creating an account with our bank!
#end
#else Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50. #end #elseif ($cmd.equals("delete")) ... #elseif ($cmd.equals("edit")) ... #end

Before we can continue we need to do some cleanup.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	#set ($firstName = $request.get("parameters").get("firstName"))
	#set ($lastName = $request.get("parameters").get("lastName"))
	#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
	#set ($date = $dateTool.getDate())

	#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
		#if ($classPK <= 0)
			#set ($classPK = $dateTool.getDate().getTime())
		#end

		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))

		#if ($cmd.equals("update"))
			Thank you, ${firstName}, for updating your account with our bank!
		#else
			Thank you, ${firstName}, for creating an account with our bank!
		#end
	#else
		Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
	#end

	#set ($classPK = 0)
#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)
#elseif ($cmd.equals("delete")) ... #elseif ($cmd.equals("edit")) ... #end

The next operation to handle is Deleting an account.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	...
#elseif ($cmd.equals("delete"))
	#if ($classPK > 0)
#set ($V = $expandoRowLocalService.deleteRow($accountsTableName, $accountsTableName, $classPK))

Account deleted!

#set ($classPK = 0)
#end
#elseif ($cmd.equals("edit")) ... #end

That's it... Pretty simple? Sure is! Next, the Edit operation.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	...
#elseif ($cmd.equals("delete"))
	...
#elseif ($cmd.equals("edit"))
	Editting...

#if ($classPK > 0)
#set ($firstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $classPK, ""))
#set ($lastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $classPK, ""))
#set ($balance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $classPK, 0.0))
#end
#end

So, we retrieved the data stored in the requested account and put them into some placeholder variables.

Finally, we're ready to display some UI elements. We'll have two different views, a table listing the accounts, and a form for adding/editing accounts.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
...
#else
...
#end

Ok, so when we show the table we need some frillies like a "Create Account" button and some column headers.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />

<br /><br />

<table class="lfr-table">
<tr>
<th>Account Number</th>
<th>First Name</th>
<th>Last Name</th>
<th>Balance</th>
<th>Modified Date</th>
<th><!----></th>
</tr>
#else ... #end

Here we're going to add the calls to get the number of, and list of all the existing accounts.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
	
	<br /><br />

	<table class="lfr-table">
	<tr>
		<th>Account Number</th>
		<th>First Name</th>
		<th>Last Name</th>
		<th>Balance</th>
		<th>Modified Date</th>
		<th><!----></th>
	</tr>

	#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))
#else ... #end

Iterate through the list.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
	
	<br /><br />

	<table class="lfr-table">
	<tr>
		<th>Account Number</th>
		<th>First Name</th>
		<th>Last Name</th>
		<th>Balance</th>
		<th>Modified Date</th>
		<th><!----></th>
	</tr>

	#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
	#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))

	#foreach($row in $rows)
#set ($currentClassPK = $row.getClassPK())

...
#end

#if ($rowsCount <= 0)
<tr>
<td colspan="5">No Accounts were found.</td>
</tr>
#end

</table>

# of Accounts: ${rowsCount}
#else ... #end

Let's draw each table row (the accounts).

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
	
	<br /><br />

	<table class="lfr-table">
	<tr>
		<th>Account Number</th>
		<th>First Name</th>
		<th>Last Name</th>
		<th>Balance</th>
		<th>Modified Date</th>
		<th><!----></th>
	</tr>

	#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
	#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))

	#foreach($row in $rows)
		#set ($currentClassPK = $row.getClassPK())

		<tr>
<td>${currentClassPK}</td>

#set ($currentFirstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $currentClassPK, ""))
<td>${currentFirstName}</td>

#set ($currentLastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $currentClassPK, ""))
<td>${currentLastName}</td>

#set ($currentBalance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $currentClassPK, 0.0))
<td align="right">${numberTool.currency($currentBalance)}</td>

#set ($currentModifiedDate = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "modifiedDate", $currentClassPK, $dateTool.getDate()))
<td>${dateFormatDateTime.format($currentModifiedDate)}</td>

<td>
<a href="${renderUrl}&amp;${namespace}cmd=edit&amp;${namespace}classPK=${currentClassPK}">Edit</a> |
<a href="${renderUrl}&amp;${namespace}cmd=delete&amp;${namespace}classPK=${currentClassPK}">Delete</a>
</td>
</tr>
#end #if ($rowsCount <= 0) <tr> <td colspan="5">No Accounts were found.</td> </tr> #end </table> # of Accounts: ${rowsCount} #else ... #end

Well, that was a mouthfull, but it should be pretty familliar design pattern. The general theme here is "ease of use". It didn't take much time or anything too tricky to get at the data.

The final piece of code is of course the input form.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	...
#else
	<form action="$renderUrl" method="post" name="${namespace}fm10">
<input type="hidden" name="${namespace}classPK" value="${classPK}" />
<input type="hidden" name="${namespace}cmd"
#if ($classPK > 0)
value="update"
#else
value="add"
#end
/>

<table class="lfr-table">
<tr>
<td>First Name:</td>
<td>
<input type="text" name="${namespace}firstName" value="${firstName}" />
</td>
</tr>
<tr>
<td>Last Name:</td>
<td>
<input type="text" name="${namespace}lastName" value="${lastName}" />
</td>
</tr>
<tr>
<td>Balance:</td>
<td>
<input type="text" name="${namespace}balance" value="${numberTool.format($balance)}" />
</td>
</tr>
</table>

<br />

<input type="submit" value="Save" />
<input type="button" value="Cancel" onclick="self.location = '${renderUrl}'" />
</form>
#end

Two significant items to note are ${renderUrl} and ${namespace}. Remember that we're running within the context of a portlet, which means that we have to get a base url from the portal (we can't just use any old url). The code we wrote earlier got us an url from the request. Secondly, we need to namespace any parameter we're going to post back to that url so that the container knows to which portlet it belongs (there might be more that one portlet on the page).

Now you have a result which should look something like this:

Creating/Editting an account:

List with 4 accounts:

Here's the whole template with comments.

#set ($locale = $localeUtil.fromLanguageId($request.get("locale")))
#set ($dateFormatDateTime = $dateFormats.getDateTime($locale))

<h1>First Expando Bank</h1>

##
## Define the "name" for our ExpandoTable.
##

#set ($accountsTableName = "AccountsTable")

##
## Get/Create the ExpandoTable to hold our data.
##

#set ($accountsTable = $expandoTableLocalService.getTable($accountsTableName, $accountsTableName))

#if (!$accountsTable)
	#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))

	#set ($accountsTableId = $accountsTable.getTableId())

	##
	## Create an ExpandoColumn for each field in the form.
	##

	#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "firstName", 15)) ## STRING
	#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "lastName", 15)) ## STRING
	#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "balance", 5)) ## DOUBLE
	#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "modifiedDate", 3)) ## DATE
#end

##
## Do some request handling setup.
##

#set ($renderUrl = $request.get("render-url"))
#set ($namespace = $request.get("portlet-namespace"))
#set ($cmd = $request.get("parameters").get("cmd"))

#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)

##
## Check to see if a classPK was passed in the request.
##

#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

##
## Check if we have received a form submission?
##

#if ($cmd.equals("add") || $cmd.equals("update"))
	##
	## Let's get the form values from the request.
	##
	
	#set ($firstName = $request.get("parameters").get("firstName"))
	#set ($lastName = $request.get("parameters").get("lastName"))
	#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
	#set ($date = $dateTool.getDate())

	##
	## Validate the params to see if we should proceed.
	##

	#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
		##
		## Check to see if it's a new Account.
		##
		
		#if ($classPK <= 0)
			#set ($classPK = $dateTool.getDate().getTime())
		#end

		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))

		##
		## Show a response.
		##
		
		#if ($cmd.equals("update"))
			Thank you, ${firstName}, for updating your account with our bank!
		#else
			Thank you, ${firstName}, for creating an account with our bank!
		#end

	#else
		Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
	#end

	#set ($classPK = 0)
	#set ($firstName = '')
	#set ($lastName = '')
	#set ($balance = 0.0)

#elseif ($cmd.equals("delete"))
	##
	## Delete the specified Row.
	##
	
	#if ($classPK > 0)
		#set ($V = $expandoRowLocalService.deleteRow($accountsTableName, $accountsTableName, $classPK))

		Account deleted!

		#set ($classPK = 0)
	#end
#elseif ($cmd.equals("edit"))
	##
	## Edit the specified Row.
	##
	
	Editting...

	#if ($classPK > 0)
		##
		## Get the account specific values
		##

		#set ($firstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $classPK, ""))
		#set ($lastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $classPK, ""))
		#set ($balance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $classPK, 0.0))
	#end
#end
	
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	##
	## Now we're into the display logic.
	##
	
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
	
	<br /><br />

	<table class="lfr-table">
	<tr>
		<th>Account Number</th>
		<th>First Name</th>
		<th>Last Name</th>
		<th>Balance</th>
		<th>Modified Date</th>
		<th><!----></th>
	</tr>

	##
	## Get all the current records in our ExpandoTable. We can paginate by passing a
	## "begin" and "end" params.
	##

	#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
	#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))

	#foreach($row in $rows)
		##
		## Get the classPK of this row.
		##

		#set ($currentClassPK = $row.getClassPK())

		<tr>
			<td>${currentClassPK}</td>

			#set ($currentFirstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $currentClassPK, ""))
			<td>${currentFirstName}</td>
		
			#set ($currentLastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $currentClassPK, ""))
			<td>${currentLastName}</td>
		
			#set ($currentBalance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $currentClassPK, 0.0))
			<td align="right">${numberTool.currency($currentBalance)}</td>
		
			#set ($currentModifiedDate = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "modifiedDate", $currentClassPK, $dateTool.getDate()))
			<td>${dateFormatDateTime.format($currentModifiedDate)}</td>
		
			<td>
				<a href="${renderUrl}&amp;${namespace}cmd=edit&amp;${namespace}classPK=${currentClassPK}">Edit</a> |
				<a href="${renderUrl}&amp;${namespace}cmd=delete&amp;${namespace}classPK=${currentClassPK}">Delete</a>
			</td>
		</tr>
	#end

	#if ($rowsCount <= 0)
		<tr>
			<td colspan="5">No Accounts were found.</td>
		</tr>
	#end

	</table>

	# of Accounts: ${rowsCount}
#else
	##
	## Here we have our input form.
	##

	<form action="$renderUrl" method="post" name="${namespace}fm10">
	<input type="hidden" name="${namespace}classPK" value="${classPK}" />
	<input type="hidden" name="${namespace}cmd"
	#if ($classPK > 0)
		value="update"
	#else
		value="add"
	#end
	/>

	<table class="lfr-table">
	<tr>
		<td>First Name:</td>
		<td>
			<input type="text" name="${namespace}firstName" value="${firstName}" />
		</td>
	</tr>
	<tr>
		<td>Last Name:</td>
		<td>
			<input type="text" name="${namespace}lastName" value="${lastName}" />
		</td>
	</tr>
	<tr>
		<td>Balance:</td>
		<td>
			<input type="text" name="${namespace}balance" value="${numberTool.format($balance)}" />
		</td>
	</tr>
	</table>

	<br />
	
	<input type="submit" value="Save" />
	<input type="button" value="Cancel" onclick="self.location = '${renderUrl}'" />
	</form>
#end

<br /><br />

Making Liferay Social - Opensocial

Staff Blogs 15 de Abril de 2008 Por Ray Augé Staff

Social networks and portals are a natural fit. To that end we're currently doing work to integrate the brand new (still in incubation) Shindig project.

Shindig is the Gadget and Opensocial reference implementation currently in incubation under the Apache Software Foundation and being worked on by Google, MySpace, Orkut, Salesforce.com, and many other large social networking sites. Liferay is now joining the fray by integrating the project.

Currently, it takes the form of a Liferay Plugin, which means it's hot deployable. As the Shindig container evolves it may become more or less integrated into Liferay depending on the direction of it's internals.

Currently, it already passes Opensocial 0.7 Compliance Tests:

Opensocial 0.7 Complience Tests


While, not officially a member of the spec team, Liferay will be following along closely and participating in ongoing development.

Announcements and Alerts

Staff Blogs 14 de Abril de 2008 Por Ray Augé Staff

In Liferay 5.0.0+ we have added a new feature: Announcements/Alerts.

Announcements/Alerts are two portlets which can be used to broadcast some message to a list of users within a known scope. Essentially they provide a mass messaging engine you'd tend to think of as a "news letter" or one-way messaging.


They provide the following features:

  1. Configurable, unlimited number of Announcement Types through portal.properties (add locatized type names via Liferay's resource bundle extension mechanism).
  2. Delivery to known scopes, called Delivery Scope, including:
    1. Individual User (API only)
    2. Role
    3. User Group
    4. Community
    5. Organization
  3. Delivery Mechanisms include Email, SMS, and Website. (Website delivery is acheived simply by adding the portlet to any page accessible to the user. The content of the portlet is allways sensitive to the viewing user.)
  4. Scheduled delivery. Each entry has a Display and Expiration Date. The entry won't be delivered until Display Date is <= now. Expiration Date is used primarily for the "Website" delivery mechanism.
  5. Read Tracking. Website delivery tracks timestamped read status per user, per entry.
  6. Subscription Control per user, per Announcement Type. A user determines which Delivery Mechanism to use for each individual Announcement Type. See the My Account page.
  7. Broadcast Control of announcements. Broadcasting Announcements is not a permission which should be granted lightly. As such, a user must first be granted Add Entry permission on the portlet to access the Announcement management functions. Secondly, a user must have Assign Member permission on a given scope in order to be able to broadcast an Announcement/Alert to that scope.

i.e. a Community Owner is not, by default, granted permission to broadcast Announcements or Alerts to their own Community. The Portal Admin must first has to grant them Add Entry permission on the portlet. Only then would they have all the permissions required (they would already have Assign Member as a function of being the owner) to broacast to their own Community. This is to prevent abuse of portal resources and prevent needless spamming of portal users.

Via the service API we now have a means of enabling event based messaging to individual users (or any delivery scope) without having to explicitly write new delivery services or functions.

There are still a few features that I'd like to see added to this portlet, but I think we now have a good base to build on.

Staging v1.2

Staff Blogs 29 de Fevereiro de 2008 Por Ray Augé Staff

(Out of Date, but still mostly relevant, has many improvements though)

Well finally it's in trunk...

 

The sponsored staging enhancements (workflow) (which I've dubbed v1.2) features are now in.

Let me know what you think.

Mostrando 41 - 60 de 69 resultados.
Items 20
de 4