« Back

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!

Threaded Replies Author Date
Thanks for the post Ray, it's great. Could you... Jorge Ferrer April 30, 2010 12:32 AM
aa Munkhzul Baatar April 30, 2010 2:13 AM
Ray - you're the best; I always learn a ton... s s April 30, 2010 9:31 AM
That's funny since I'm in the beautiful Sudbury... Ray Augé April 30, 2010 9:38 AM
Hi! How do i sort say $rows (from #set ($rows... Christian De Vera Talampas May 26, 2010 8:56 PM
Hmm, this is tougher. In fact it's one of the 2... Ray Augé May 27, 2010 7:03 PM
thanks ray! i'll keep that in mind. Christian De Vera Talampas May 31, 2010 12:15 AM
Hi Ray, Great article, helped me understand... Ashish Gupta September 8, 2010 8:45 AM
I tried the new Expando code for 6.0.5 just for... Flavel --------- September 21, 2010 10:07 AM
You must make sure that on the Template... Ray Augé September 21, 2010 10:24 AM
Gah! Right when I saw "Cacheable" I realized I... Flavel --------- September 22, 2010 5:06 AM
Hi Ray, I have the same need as A. Gupta : ... Christophe Cariou May 19, 2011 5:47 AM
Custom fields are indexed using a namespace (so... Ray Augé May 19, 2011 6:07 AM

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
Posted on 4/30/10 12:32 AM.
Posted on 4/30/10 2:13 AM.
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!
Posted on 4/30/10 9:31 AM.
That's funny since I'm in the beautiful Sudbury region (Ontario) emoticon

And thanks!
Posted on 4/30/10 9:38 AM in reply to Stephen Skinner.
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
Posted on 5/26/10 8:56 PM in reply to Ray Augé.
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.
Posted on 5/27/10 7:03 PM in reply to Christian De Vera Talampas.
thanks ray! i'll keep that in mind.
Posted on 5/31/10 12:15 AM in reply to Ray Augé.
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/ma­ximized#_19_message_5807724> Search documents by custom field values</A>
Would you have any idea on how to do this ?
Thanks
-Ashish
Posted on 9/8/10 8:45 AM.
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.
Posted on 9/21/10 10:07 AM.
You must make sure that on the Template configuration to uncheck "Cacheable"!
Posted on 9/21/10 10:24 AM in reply to Flavel ---------.
Gah! Right when I saw "Cacheable" I realized I had seen this in other places and forums. Works like a charm now, Thanks!
Posted on 9/22/10 5:06 AM in reply to Ray Augé.
Hi Ray,

I have the same need as A. Gupta :

How can we use extended attributes in a lucene search ?
Posted on 5/19/11 5:47 AM in reply to Flavel ---------.
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.
Posted on 5/19/11 6:07 AM in reply to Christophe Cariou.