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

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!

Blogs
Thanks for the post Ray, it's great.

Could you add this info and update the following wiki page too?

http://www.liferay.com/community/wiki/-/wiki/Main/Developing+with+Expando
Ray - you're the best; I always learn a ton from your examples. If you ever feel like a few days of consulting in beautiful downtown Kingston (Ontario) let me know!
That's funny since I'm in the beautiful Sudbury region (Ontario) emoticon

And thanks!
Hi!

How do i sort say $rows (from #set ($rows = $expandoRowLocalService.getRows($accountsTableId, $start, $end)))? I want to sort "balance" in descending order. Is there a way? The sorter tool seems not working for me.

Thanks
Hmm, this is tougher. In fact it's one of the 2 toughest things I have yet to introduce; an integrated way to do filtering and sorting. Both of these I have ideas for, but they might not appear until 6.1 (unless I decide to do it as a plugin before so it can be used in 5.2).

Until then, you will have to do the sorting, post query, the old fashioned way (get all the results, and then sort them after the fact using a custom comparator implementation) and hopefully you're data set is not too massive.
Hi Ray,
Great article, helped me understand Expando quite a bit, but I do have a different but related question about searching based on custom attributes, I created another thread for it. Here's the link <a href=http://www.liferay.com/community/forums/-/message_boards/message/5807724/maximized#_19_message_5807724> Search documents by custom field values</A>
Would you have any idea on how to do this ?
Thanks
-Ashish
I tried the new Expando code for 6.0.5 just for kicks. Everything appeared on the page appropriately and in the database. I went to click the add accounts button, but it just refreshes the page (doesn't actually let me add an account). Was there a different structure I should be using besides: Name = "Basic", Added a row called "content" of type "text". Thanks again for the example.
You must make sure that on the Template configuration to uncheck "Cacheable"!
Gah! Right when I saw "Cacheable" I realized I had seen this in other places and forums. Works like a charm now, Thanks!
Hi Ray,

I have the same need as A. Gupta :

How can we use extended attributes in a lucene search ?
Custom fields are indexed using a namespace (so as they won't collide with actual entity fields).

The encoding is "expando/custom_fields/<fieldName>".

So, if you wanted to search for a specific custom field's value you could do:

+(+companyId:12345 +expando/custom_fields/favoriteColor:blue)

Note that in 6.0.x (the next SP) and in 6.1 we have custom field searching enabled for all fields on all indexed entities enabled by default (so when you do a basic keyword search, it will also search on expando fields).

Also (in those versions), in the basic search, if you were to perform a keyword search including the encoded <fieldName>, like this:

expando/custom_fields/favoriteColor:blue

it would also work. Realizing that this is not so user friendly it does mean that if you were to add a input field that passed a value for this field and then added it encoded this way to the "keywords" variable used in the search, it would work as you expect.