Setting tabwidths to match the project settings in GIT and VI

Company Blogs May 19, 2010 By Ray Augé Staff

I use Git and VI(m) quite a bit, and in the Liferay project the source format rules define that leading whitespace character is tab (with potentially trainling spaces before the rest of the line in some cases, but that's another story).

Furthermore, tab width is 4.

Since I'm commonly in the command line I hate that the source doesn't represent the same way that I see it in the IDE, especially when reviewing diffs since most console output has tab width of 8.

GIT

So the question is "How do I make git output use tab width of 4 instead of the default 8?"

After searching long without success, I finnally learned that git uses the "pager" paradigm for displaying diffs, and that the external pager can be tuned in the ~/.gitconfig file.

Awesome!

So, to adjust the tab width add the following to your config:

[core]
    pager = less -FXRS -x4

 

This tells git to use "less" command as the external pager, then it passes some settings to less, but most importantly it sends the setting:

-x<n>

which sets the tab width.

VI(m)

Now to get the same result in VI(m) you can edit the ~/.vimrc file. Coolness!

set tabstop=4
set ts=4
set shiftwidth=4
set autoindent

 

Great! That's it.

Now all my important console output has the proper tab widths (and I can tell if I messed up my bchan style formatting).  

;)

Git the most out of your bash prompt

Company Blogs May 14, 2010 By Ray Augé Staff

I was looking for a way to add as much relevant info about my Liferay git repository to my prompt as would not slow it down to much, cause I hated having to constantly do git branch/status to find branch/state all the time.

So after reading a couple nice posts about it,

http://henrik.nyh.se/2008/12/git-dirty-prompt

http://plasti.cx/2009/10/23/vebose-git-dirty-prompt

I took the best of both of those (performance wise and output wise), and since I use git-svn added to mine the requirement of showing the svn revision my branch is currently synced with.

Here is the bash code I added to by ~/.bashrc :

function parse_git_dirty {
status=`git status 2> /dev/null`
        dirty=`    echo -n "${status}" 2> /dev/null | grep -q "Changed but not updated" 2> /dev/null; echo "$?"`
        untracked=`echo -n "${status}" 2> /dev/null | grep -q "Untracked files" 2> /dev/null; echo "$?"`
        ahead=`    echo -n "${status}" 2> /dev/null | grep -q "Your branch is ahead of" 2> /dev/null; echo "$?"`
        newfile=`  echo -n "${status}" 2> /dev/null | grep -q "new file:" 2> /dev/null; echo "$?"`
        renamed=`  echo -n "${status}" 2> /dev/null | grep -q "renamed:" 2> /dev/null; echo "$?"`
        bits=''
        if [ "${dirty}" == "0" ]; then
                bits="${bits}☭"
        fi
        if [ "${untracked}" == "0" ]; then
                bits="${bits}?"
        fi
        if [ "${newfile}" == "0" ]; then
                bits="${bits}*"
        fi
        if [ "${ahead}" == "0" ]; then
                bits="${bits}+"
        fi
        if [ "${renamed}" == "0" ]; then
                bits="${bits}>"
        fi
        echo "${bits}"
}

function parse_git_svn_revision {
        ref1=$(__git_ps1 | sed -e "s/ (\(.*\))/(git: \1$(parse_git_dirty))/")
        #ref1=$(parse_git_branch)

        if [ "x$ref1" != "x"  ]; then
                ref2=$(git svn info | grep Revision)
                echo " ${ref1} (svn: r"${ref2#Revision: }") "
        fi
}

PS1='\[\033[0;37m\][\[\033[0;31m\]\u@\h\[\033[0;33m\]`parse_git_svn_revision`\[\033[0;32m\]\W\[\033[0;37m\]]\$ '

 
This is what it looks like:

the output of a bash session using the new prompt

 

As per Plasticx's blog the dirty flags are as such:

  • ‘☭’ – files have been modified
  • ‘?’ – there are untracted files in the project
  • ‘*’ – a new file has been add to the project but not committed
  • ‘+’ – the local project is ahead of the remote
  • ‘>’ – file has been moved or renamed

 

Much better!

Expandos II (refactor of a previous post for 6.0+)

Company Blogs April 29, 2010 By Ray Augé Staff

Quite a while ago I wrote an article on Liferay's Expandos (on top of which our Custom Attributes (Custom Fields) are built), using the WCM as the runtime for a small app called First Expando Bank.

Since that time the API has undergone slight alterations and so since there was so much interest in it, here is new implementation which does a better job of showing how to use the API by:

  1. Making less service calls
  2. Demonstrating Pagination

Here is the updated template:

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

#set ($companyId = $getterUtil.getLong($request.theme-display.company-id))
#set ($locale = $localeUtil.fromLanguageId($request.locale))
#set ($dateFormatDateTime = $dateFormats.getDateTime($locale))

#set ($renderUrl = $request.render-url)
#set ($pns = $request.portlet-namespace)
#set ($cmd = $getterUtil.getString($request.parameters.cmd))

#set ($cur = $getterUtil.getInteger($request.parameters.cur, 1))
#set ($delta = $getterUtil.getInteger($request.parameters.delta, 5))

#set ($end = $cur * $delta)
#set ($start = $end - $delta)

<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($companyId, $accountsTableName, $accountsTableName))
#set ($accountsTableId = $accountsTable.getTableId())

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

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

#set ($accountsTableClassNameId = $accountsTable.getClassNameId())
#set ($columns = $expandoColumnLocalService.getColumns($accountsTableId))

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

#set ($classPK = $getterUtil.getLong($request.parameters.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 = $getterUtil.getString($request.parameters.firstName, ''))
	#set ($lastName = $getterUtil.getString($request.parameters.lastName, ''))
	#set ($balance = $getterUtil.getDouble($request.parameters.balance, 0.0))
	#set ($date = $dateTool.getDate())

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

	#if ($balance < 50)
		Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
	#elseif (!$firstName.equals('') && !$lastName.equals(''))
		##
		## Check to see if it's a new Account.
		##

		#if ($classPK <= 0)
			#set ($classPK = $dateTool.getDate().getTime())
		#end

		#set ($VOID = $expandoValueLocalService.addValues($accountsTableClassNameId, $accountsTableId, $columns, $classPK, {'firstName':$firstName, 'lastName':$lastName, 'balance':"$balance", 'modifiedDate':"${date.getTime()}"}))

		##
		## 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. Make sure to till both first and last name fields.
	#end
#elseif ($cmd.equals('delete'))
	##
	## Delete the specified Row.
	##

	#if ($classPK > 0)
		#set ($VOID = $expandoRowLocalService.deleteRow($accountsTableId, $classPK))

		Account deleted!

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

	Editting...
#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}&${pns}cmd=edit';" />

	<br /><br />

	<table class="taglib-search-iterator">
	<tr class="results-header">
		<th class="col-1">Account Number</th>
		<th class="col-2">First Name</th>
		<th class="col-3">Last Name</th>
		<th class="col-4">Balance</th>
		<th class="col-5">Modified Date</th>
		<th class="col-6"><!----></th>
	</tr>

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

	#set ($total = $expandoRowLocalService.getRowsCount($accountsTableId))
	#set ($rows = $expandoRowLocalService.getRows($accountsTableId, $start, $end))

	#foreach($row in $rows)
		#set ($cssClass = 'results-row')

		#if ($velocityCount % 2 == 0)
			#set ($cssClass = "${cssClass} alt")
		#end

		#if ($velocityCount == 1)
			#set ($cssClass = "${cssClass} first")
		#elseif ($velocityCount == $rows.size())
			#set ($cssClass = "${cssClass} last")
		#end

		##
		## Get the classPK of this row.
		##

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

		#set ($rowValues = $expandoValueLocalService.getRowValues($row.getRowId()))

		#set ($values = {})

		#foreach ($value in $rowValues)
			#foreach ($column in $columns)
				#if ($value.columnId == $column.columnId)
					#set ($VOID = $values.put($column.name, $value))
				#end
			#end
		#end

		#set ($currentFirstName = $values.firstName.string)
		#set ($currentLastName = $values.lastName.string)
		#set ($currentBalance = $values.balance.double)
		#set ($currentModifiedDate = $values.modifiedDate.date)

		<tr class="${cssClass}">
			<td class="align-left col-1 valign-left">${currentClassPK}</td>

			<td class="align-left col-2 valign-middle">${currentFirstName}</td>

			<td class="align-left col-3 valign-middle">${currentLastName}</td>

			<td class="align-right col-4 valign-middle">${numberTool.currency($currentBalance)}</td>

			<td class="align-left col-5 valign-middle">${dateFormatDateTime.format($currentModifiedDate)}</td>

			<td class="align-right col-6 valign-middle">
				<a href="${renderUrl}&amp;${pns}cmd=edit&amp;${pns}classPK=${currentClassPK}">Edit</a> |
				<a href="${renderUrl}&amp;${pns}cmd=delete&amp;${pns}classPK=${currentClassPK}">Delete</a>
			</td>
		</tr>
	#end

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

	</table>

	<br/>

	#if ($total > $delta)
		<div style="float: right;">
			<div>
				#set ($previous = $cur - 1)
				#set ($next = $cur + 1)

				#if ($previous > 0)
					<a href="${renderUrl}&${pns}cur=${previous}" class="previous">‹‹ #language('previous')</a>
				#else
					<span class="previous">‹‹ #language('previous')</span>
				#end

				#set ($pagesIteratorBegin = 1)
				#set ($pagesIteratorEnd = $total / $delta)
				#if (($total % $delta) > 0)
					#set ($pagesIteratorEnd = $pagesIteratorEnd + 1)
				#end

				#foreach ($index in [$pagesIteratorBegin..$pagesIteratorEnd])
					#if ($index == $cur)
						#set ($pageNumber = "<strong>${index}</strong>")
					#else
						#set ($pageNumber = $index)
					#end

					<a href="${renderUrl}&${pns}cur=${index}" class="previous">${pageNumber}</a>
				#end

				#if ($next > $cur && $next <= $pagesIteratorEnd)
					<a href="${renderUrl}&${pns}cur=${next}" class="next">#language('next') ››</a>
				#else
					<span class="next">#language('next') ››</span>
				#end
			</div>
		</div>
	#end

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

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

		#set ($rowValues = $expandoValueLocalService.getRowValues($companyId, $accountsTableName, $accountsTableName, $classPK, -1, -1))

		#set ($values = {})

		#foreach ($value in $rowValues)
			#foreach ($column in $columns)
				#if ($value.columnId == $column.columnId)
					#set ($VOID = $values.put($column.name, $value))
				#end
			#end
		#end

		#set ($currentFirstName = $values.firstName.string)
		#set ($currentLastName = $values.lastName.string)
		#set ($currentBalance = $values.balance.double)
	#end

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

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

	<br />

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

<br /><br />
Expando Bank 2 Image

Enjoy!

Mobile, Point of Sale, Kiosk, Terminal Server, Thin Client, the Web and Liferay

Company Blogs November 21, 2009 By Ray Augé Staff

Do you maintain/use a Mobile delivered App?

Do you maintain/use a Point of Sale delivered App?

Do you maintain/use a Kiosk delivered App?

Do you maintain/use a Terminal Server delivered App?

Do you maintain/use a Thin Client delivered App?

Do you maintain/use a standalone or embeddable Web App or Widget?

 

If you can answer yes to anyone of these questions, have you considered that Liferay can be used in all these scenarios? Probably all at the same time?

In fact, did you know that Liferay has OOTB features which help you to develop such applications?

Oh, we don't have a feature that you require in order to address some application specific concern? We're open source... We or you can add it.

Liferay is not just about aggregation as most other portals are. Liferay is also about delivery.

  • Mobile
  • WSRP
  • Facebook App
  • Google Gadget
  • Net Vibes
  • Embedded HTML Widget
  • Desk-Web App (Chrome, Prism, Air)

It's all there for you. We've done them all.

Using setenv.sh to configure tomcat env AND Debugging

Company Blogs October 30, 2009 By Ray Augé Staff

I know that some people are suggesting using the setenv.sh script to control JVM settings for tomcat.

Notably I see adding JMX configurations this way, among other things.

Be careful what you add to this script as some things, like JMX settings, will break the ability to use the shutdown scripts.

The setenv.sh script is called regardless of startup or shutdown of tomcat. If you add JMX for instance, it tries to bind the JVM's JMS server to a port. While this sounds ok, during shutdown you don't need this, plus it will cause the JVM you're starting up to pooch because the port is already bound by another JVM (the one you're trying to shutdown).

Also, simply adding these settings to catalina.sh can cause the same problem.

The solution is to isolate things you only want tomcat to have during startups. You can use this if block, right around line 188 of catalina.sh:

...
if [ "$1" != "stop" ] ; then
  JAVA_OPTS="$JAVA_OPTS -Xms1024m -Xmx1024m -XX:PermSize=128m -XX:MaxPermSize=256m -Xshare:off"
  JAVA_OPTS="$JAVA_OPTS -Dsolr.solr.home=/home/some/deploy-sp/solr-1.3"
  JAVA_OPTS="$JAVA_OPTS -Dexternal-properties=/home/some/portal-web/docroot/WEB-INF/src/portal-developer.properties"
  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote=true"
  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=9030"
  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false"
  JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
fi

# ----- Execute The Requested Command -----------------------------------------
...

That should allow the shutdown scripts to work again as expected.

Oh, one more thing... you don't have to add debug startup parameters to tomcat's startup scripts BTW.

i.e. -agentlib:jdwp=transport=....

Tomcat already provides a mechanism to startup up so you can connect with a debugger.

Simply start with catalina.sh instead like so:

$ ./catalina.sh jpda start

The JVM is now running with appropriate debug configuration at port 8000 by default.

Can Liferay & COSS make you smarter?

Company Blogs October 20, 2009 By Ray Augé Staff

I've recently been reading Pragmatic Thinking & Learning: Refactor Your Wetware by Any Hunt, Pragmatic Bookshelf, and it has really got me thinking... (Yes, that was pretty lame. HaHaHa!)

Actually it got me thinking on "context", "perspective", "metaphors", and in particular the definition of "Pair programming", which struck me as interesting. Pair programming is a highly effective technique used to improve the productivity and quality of work generated by the pair. As Mr. Hunt demonstrates in his book, it benefits from cognitive side effects that may in fact cause a person to be a little bit smarter when working as a pair rather than when working on your own.

Here is the definition as found on WikiPedia:

 

"Pair programming is a software development technique in which two programmers work together at one work station. One types in code while the other reviews each line of code as it is typed in. The person typing is called the driver. The person reviewing the code is called the observer or navigator... While reviewing, the observer also considers the strategic direction of the work, coming up with ideas for improvements and likely future problems to address. This frees the driver to focus all of his or her attention on the "tactical" aspects of completing the current task, using the observer as a safety net and guide."

 

While that definition doesn't mention anything about causing one to be any smarter, what I find stricking is that it does mention things like "considers the strategic direction of the work, coming up with ideas for improvements and likely future problems to address", and "focus all of his or her attention on the "tactical" aspects of completing the current task" which I would quite confidently define as working smarter any day of the week.

The next thing that I find stricking is how closly it resembles a trend in software development which we call "Commercial Open-Source" . How do these two things relate?

Let's re-write the definition and only change a few of key words:

 

"Commercial Open-Source is a software development practice in which two entities work together on one repository of source code. One types in code while the other reviews each line of code as it is typed in. The entity typing is called the company. The entity reviewing the code is called the community. While reviewing, the community also considers the strategic direction of the work, coming up with ideas for improvements and likely future problems to address. This frees the company to focus all of its attention on the "tactical" aspects of completing the current task, using the community as a safety net and guide."

 

This sounds relatively accurate and makes that Pair programming is a pretty great metaphor for Commercial Open Source.

What does that mean exactly?

It might mean that while working on Commercial Open Source projects, like Liferay, the community is a little bit smarter, the company is a little bit smarter. It might mean than Commercial Open Source projects, like Liferay, are a little bit better for it.

It also means that it's in the best interest of both company and community to ensure and nurture a lasting relationship such that the pair can continue to be effective and continue to be smarter.

Until next time, let's all get smarter together!

Rule #57 of Liferay Theme Design (for non-theme guys like me)

Company Blogs October 19, 2009 By Ray Augé Staff

Have you ever had a super freakish problem with the Liferay CSS Not doing what you expected? I mean I love Liferay's new Minifier, it's fast and efficient. But, it's no fun when you have problems.

Suppose you have some css:

.something-a .something-b,
.something-c .something-d,
.ie6 .something-e {
  ... do this ...
}

.something-g {
  ... do that ...
}

Looks innocent enough, right?

NOPE!

 

It's not innocent! Inside there lies a develish beast that is waiting to bite your earlobes off. But really he's supposed to be your friend... he just needs to be handled with certain amount of firmness, tough love so to speak. You have to set and keep some rules. Otherwise, he'll misbehave.

He'll reduce the above css on a non-ie6 browser to this:

.something-a .something-b,
.something-c .something-d,
.something-g {
  ... do that ...
}

which is NOT what you wanted at all, am I right?

Rule #57 of Liferay Theme Design is: Never, ever, ever mix browser css selectors with non-browser selector css definitions! Always break them out like this:


.something-a .something-b,
.something-c .something-d
{
  ... do this ...
}


.ie6 .something-e {

  ... do this ...
}

.something-g {
  ... do that ...
}

There, now you have laid down the law! Mr. Minifier will handle your css appropriately.

Adding Expandos from a Startup Hook

Company Blogs October 9, 2009 By Ray Augé Staff

I've seen a tone of requests lately for how to add Expando Tables and Columns programatically during some startup proceedure.

The biggest problem seems to be with PermissionChecker. The problem stems from the fact that alot of people are trying to use the ExpandoBridge API to create columns (attributes) during the startup process. The issue with that is that the default implementation of ExpandoBirdge has permission checking built in. This leads to exceptions because there is no PermissionChecker yet bound to the thread during startup.

Have no fear. There is a solution! And, it was written by none other than Brian "The Man" Chan himself, so it's gotta be right. He did this in the WOL portlet plugin that you all know from Liferay.com, so it's even in production.

Here it is verbatim from the 5.2.x branch in SVN:

package com.liferay.wol.hook.events;

import com.liferay.portal.kernel.events.ActionException;
import com.liferay.portal.kernel.events.SimpleAction;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.model.User;
import com.liferay.portlet.expando.DuplicateColumnNameException;
import com.liferay.portlet.expando.DuplicateTableNameException;
import com.liferay.portlet.expando.model.ExpandoColumnConstants;
import com.liferay.portlet.expando.model.ExpandoTable;
import com.liferay.portlet.expando.service.ExpandoColumnLocalServiceUtil;
import com.liferay.portlet.expando.service.ExpandoTableLocalServiceUtil;

/**
 * <a href="StartupAction.java.html"><b><i>View Source</i></b></a>
 *
 * @author Brian Wing Shun Chan
 *
 */
public class StartupAction extends SimpleAction {

	public void run(String[] ids) throws ActionException {
		try {
			doRun(GetterUtil.getLong(ids[0]));
		}
		catch (Exception e) {
			throw new ActionException(e);
		}
	}

	protected void doRun(long companyId) throws Exception {
		setupExpando();
	}

	protected void setupExpando() throws Exception {
		ExpandoTable table = null;

		try {
			table = ExpandoTableLocalServiceUtil.addTable(
				User.class.getName(), "WOL");
		}
		catch (DuplicateTableNameException dtne) {
			table = ExpandoTableLocalServiceUtil.getTable(
				User.class.getName(), "WOL");
		}

		try {
			ExpandoColumnLocalServiceUtil.addColumn(
				table.getTableId(), "jiraUserId",
				ExpandoColumnConstants.STRING);
		}
		catch (DuplicateColumnNameException dcne) {
		}

		try {
			ExpandoColumnLocalServiceUtil.addColumn(
				table.getTableId(), "aboutMe", ExpandoColumnConstants.STRING);
		}
		catch (DuplicateColumnNameException dcne) {
		}
	}

}

Now that is some sweet code!

One small note. These columns were added to a table called "WOL". As such these are NOT what we ferrer to as Custom Attributes.

There needs to be one small change made in order for the ExpandoColumns above to be considered Custom Attributes. They must be added to a special table called "DEFAULT_TABLE" (a.k.a. ExpandoTableConstants.DEFAULT_TABLE_NAME).

		try {
			table = ExpandoTableLocalServiceUtil.addTable(
				User.class.getName(), ExpandoTableConstants.DEFAULT_TABLE_NAME);
		}
		catch (DuplicateTableNameException dtne) {
			table = ExpandoTableLocalServiceUtil.getTable(
				User.class.getName(), ExpandoTableConstants.DEFAULT_TABLE_NAME);
		}

Columns in this "DEFAULT_TABLE" are the ones which are retreived and manipulated by the ExpandoBridge API (more on that API in another blog post).

G[rease]Mail

Company Blogs May 31, 2009 By Ray Augé Staff

Well, I decided this week, while I was "in the field" that I would give GMail a try. I've had an account for a long time now, but I'm used to having everything local on a machine where I store gigs and gigs of mail from at least the past 8-9 years.

I'm a email pack-rat I guess.. but it has saved my butt more than once.

But, it's hard to carry around a desktop just because it has all my email.. and it's hard to not do email when you're not near that desktop. I had to bite the bullet and do something radical like resolve myself to permanently use a remote mail service, namely GMail (I also have Fusemail, which sadly I long ago suggested we use for our work email platform, and has not been the astounding success it was supposed to be).

So GMail, here I come. First things first. GMail is far from perfect, but it has enough features to suite my needs. My biggest complaint was with wasted realestate; bad column widths, font sizes, ads, etc.

Luckily I only ever use Firefox, so "Greasemonkey to the rescue". I installed the plugin and started hacking and testing with firebug to locate the offending elements.

As a result I produced the G[rease]Mail greasemonkey user script, the result of which is the start of a nice clean GMail UI:

 

 Now, maybe I can start to like GMail a little more.

My simple greasemonkey GMail UI cleanup script: G[rease]Mail

Enjoy!

Loving the new machine...

Company Blogs April 30, 2009 By Ray Augé Staff

Well, over the last couple years I've been largely working from a laptop. I thought I liked it... until recently I simply needed more power.

I went ahead and did some research and found that there was an amazing deal out there that was simply too good to pass up.

The Dell Studio XPS, featuring the new Intel i7 series processor. This thing is simply amazing. It features:

  • 8 cores @ 8MB L2 Cache, 2.66GHz
  • 12GB Tri-Channel DDR3 SDRAM at 1066MHz

 

Another look.

I hate to sound like I'm bragging about this... I was always years behind the bleeding edge when it came to hardware. It's the first time I've treated myself to a decent machine, and I'm very happy.

To anyone looking for a high performance at a fairly low price point, get one of these... for less that $2k CDN it's a steel.

Patents, crimes against humanity!

Company Blogs March 16, 2009 By Ray Augé Staff

[PS: this is my personal opinion and should not reflect in anyway the great company I work for.]

Quite often we hear the saying that a person is the product of their environment. We all know that everyone is unique and that this uniqueness boiled together with the events and experiences of a lifetime which make for truely unique individuals.

That being said, I often ponder these:

1) Are ideas our own? Truely and fully our own? They are influenced by eventhing around us. The same experiences that shape us and help mold our character are what help us form ideas. So, doesn't that mean that you share a little bit of the ownership of an idea with everyone and everything that caused you to develop it? Without them you likely would not have perceived it to begin with.

2) If you have an idea which can better the world around you, and you do not share it, are you steeling from those who helped you get to the point where you perceived it? If the great scientists of the past had not shared their ideas, where would our world be today?

I see patents as a form of theft against nature, against humanity. When a person claims sole ownership of, and imposes restrictions on an idea they are stating that they have not been influenced by the world in any way, they are deliberately making an attempt to subvert the natural progress of humanity by limiting the impact the idea can have on the next generation of thinkers who they themselves must be influenced by the world around them. This next generation is crippled in the sense that they have been physically restricted from venturing down a stream of thought which in some way expands of the original idea. They are in reality saying "Don't even think about it!" and meaning it in the truest sense.

I would never take a way the credit one deserves for the ideas they perceive, but it's simply dishonest to think they perceived them solely on their own. Thus any patent is a crime against humanity as it is solely designed to inhibit its use by others. Though perhaps not the goal of patents, their result is to deliberately impose further disabilities on humanity. We already have enough of those.

Deploying plugins to Liferay

Company Blogs March 13, 2009 By Ray Augé Staff

Alot of people seem to have problems deploying custom plugins to liferay. I admit that deployment can be a little tricky.

First off, Liferay has a naming convention which is more than just a naming convention and is actually designed to impose behavior. I think that in the future this should be changed to be based strictly on metadata contained in descriptors.

Anyway, that doesn't help you right now. So, plugins come in 5 types:

  • hooks
  • layout templates
  • portlets
  • themes
  • webs

So, when creating plugins projects the naming of your project dir should be:

project-name-{type-sufix}

where {type-sufix} is one of

  • hook
  • layouttpl
  • portlet
  • theme
  • web

This is done automatically when using the ant tasks or create scripts in the SDK. But if you created your project from scratch but still using the liferay build scripts, then this is where you fall into the mis-naming trap.

If you don't follow this convention one potential side effect is that you end up with a deployed project with a context like:

webapps/project-name-5.1.3.7

instead of just

webapps/project-name

This is because Liferay doesn't recognize the "type" of plugin and assumes it's a plain old webapp and doesn't change the context path.

Now, sometimes your code is already written OR you are including something inside the plugin that WANTs to use a specific context path. How do you deal with that? Liferay jas a little known feature to handle this. Simply add the following in the liferay-plugin-package.properties file.

recommended-deployment-context={desired-context-name}

This value will be used as the context path of the deployed plugin.

Enjoy!

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

Company Blogs March 5, 2009 By 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

Company Blogs March 5, 2009 By 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

Company Blogs February 18, 2009 By Ray Augé Staff

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

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

Let's start with the interface:

package com.mytool;

public interface MyTool {

	public String operationOne();

	public String operationTwo(String name);

}

The util class:

package com.mytool;

public class MyToolUtil {

	public static MyTool getMyTool() {
		return _myTool;
	}

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

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

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

	private static MyTool _myTool;

}

The implementation class:

package com.mytool;

public class MyToolImpl implements MyTool {

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

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

}

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

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

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

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

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

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

$myTool.operationOne()

$myTool.operationTwo('Ray')

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

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

$myTool.operationOne()

$myTool.operationTwo('Ray')

Enjoy!

Performance testing using a nifty portal tool

Company Blogs February 2, 2009 By 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

Company Blogs January 9, 2009 By 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

Company Blogs October 7, 2008 By 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)

Company Blogs September 23, 2008 By 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

Company Blogs September 23, 2008 By 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.

Showing 41 - 60 of 81 results.
Items 20
of 5