Updated: Wed May 28 10:53:09 EDT 2008.
See the follow-up article here.
In Javascript, "expando" means "to attach additional properties to an object".
That's a little bit of hint as to what Expandos are in Liferay.
In Liferay the Expando service is a "generic" service which allows you to dynamically define a collection of data. This data can be
- typed (boolean, Date, double, int, long, short, String, and arrays of all those)
- associated with a specific entity (e.g. 'com.liferay.portal.model.User')
- arranged into any number of "columns"
- available to plugins
- accessed from Velocity templates
- accessed via Liferay's JSON API through AJAX
The service also provides all the CRUD methods you typically need (Create/Retreive/Update/Delete).
You often need a way to add some custom field to Users or other portal entities and sometimes you need to do it fast and with minimal effort... it might even be temporary. Well, Expando is comming to the rescue.
To demonstrate how Expando works, I'll take you through a sample application which runs completely in as a velocity Journal Template. 
So first off, lets get the required details out of the way.
Step 1) Create a Journal Structure. The structure is a requirement of any Journal Template because the connection between a Journal Article and a Journal Template is a function of the Journal Structure.
A very basic Structure is all we need in this case:
<root> <dynamic-element name='content' type='text'></dynamic-element> </root>
Step 2) Create our Template
Our template "Language Type" will be VM (for Velocity), and we'll disable template caching by unckecking "Cacheable".
We'll start simple and work our way through the code. The first few lines will get some utility objects that we'll use later, and set a title for our app.
#set ($locale = $localeUtil.fromLanguageId($request.get("locale")))
#set ($dateFormatDateTime = $dateFormats.getDateTime($locale))
<h1>First Expando Bank</h1>
Step 3) We create our Article.
On the Article tab we essentially click "Add Article", give it a "Name", choose the Structure we created in Step 1) and click "Save".
The result should be something like this:
Now, the first task when using Expando is to define our table. Expando lets you do this programatically and with very little code.
Give our table a name.
...
<h1>First Expando Bank</h1>
#set ($accountsTableName = "AccountsTable")
Check to see if the table exists, and if not, create it.
...
#set ($accountsTableName = "AccountsTable")
#set ($accountsTable = $expandoTableLocalService.getTable($accountsTableName, $accountsTableName))
#if (!$accountsTable)
#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))
#end
We now have a table and we want to add columns to it. Since we're building a Bank app fields we need are firstName, lastName, and balance. We'll also keep track of the last time the account was updated, modifiedDate. The account number will be automitically generated and will represent the primary key for our table. In Expando, this primary key doesn't require a standalone column.
Also, we don't want this process to happen every time, so we'll only do it when the table is first created.
...
#if (!$accountsTable)
#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))
#set ($accountsTableId = $accountsTable.getTableId())
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "firstName", 15)) ## STRING
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "lastName", 15)) ## STRING
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "balance", 5)) ## DOUBLE
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "modifiedDate", 3)) ## DATE
#end
Notice how we specified, for each column, an integer as the last parameter. These integer constants are defined in com.liferay.portlet.expando.model.ExpandoColumnConstants, and include the types I mentioned above.
Now that we have our table and columns setup, we want to add some logic that will detect and handle the various operations of our application. These are the CRUD operations we need for a complete app.
Let's start with some request handling and param setup.
...
#set ($renderUrl = $request.get("render-url"))
#set ($namespace = $request.get("portlet-namespace"))
#set ($cmd = $request.get("parameters").get("cmd"))
#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)
I won't go into much detail regarding the request handling abilitites of Journal Templates. Suffice it to say that it supports it. (I'll cover that in another Blog entry.)
Determine whether we have been passed an account number. As I mentioned earlier, Expando "records" have a primary key. That field is called "classPK", so we will keep that name to re-inforce the concept.
...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
Now we're going to check and see what operation (if any) we were asked to perform.
...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
#if ($cmd.equals("add") || $cmd.equals("update"))
...
#elseif ($cmd.equals("delete"))
...
#elseif ($cmd.equals("edit"))
...
#end
Adding/Updating an account is our first operation. In this case, we get the params from the request.
...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
#if ($cmd.equals("add") || $cmd.equals("update"))
#set ($firstName = $request.get("parameters").get("firstName"))
#set ($lastName = $request.get("parameters").get("lastName"))
#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
#set ($date = $dateTool.getDate())
#elseif ($cmd.equals("delete"))
...
#elseif ($cmd.equals("edit"))
...
#end
Do some form input checking (this one just does a basic check).
...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
#if ($cmd.equals("add") || $cmd.equals("update"))
#set ($firstName = $request.get("parameters").get("firstName"))
#set ($lastName = $request.get("parameters").get("lastName"))
#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
#set ($date = $dateTool.getDate())
#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
...
#else
Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
#end
#elseif ($cmd.equals("delete"))
...
#elseif ($cmd.equals("edit"))
...
#end
So now, if we're ok, store the data.
...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
#if ($cmd.equals("add") || $cmd.equals("update"))
#set ($firstName = $request.get("parameters").get("firstName"))
#set ($lastName = $request.get("parameters").get("lastName"))
#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
#set ($date = $dateTool.getDate())
#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
#if ($classPK <= 0)
#set ($classPK = $dateTool.getDate().getTime())
#end
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))
#if ($cmd.equals("update"))
Thank you, ${firstName}, for updating your account with our bank!
#else
Thank you, ${firstName}, for creating an account with our bank!
#end
#else
Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
#end
#elseif ($cmd.equals("delete"))
...
#elseif ($cmd.equals("edit"))
...
#end
Before we can continue we need to do some cleanup.
...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
#if ($cmd.equals("add") || $cmd.equals("update"))
#set ($firstName = $request.get("parameters").get("firstName"))
#set ($lastName = $request.get("parameters").get("lastName"))
#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
#set ($date = $dateTool.getDate())
#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
#if ($classPK <= 0)
#set ($classPK = $dateTool.getDate().getTime())
#end
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))
#if ($cmd.equals("update"))
Thank you, ${firstName}, for updating your account with our bank!
#else
Thank you, ${firstName}, for creating an account with our bank!
#end
#else
Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
#end
#set ($classPK = 0)
#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)
#elseif ($cmd.equals("delete"))
...
#elseif ($cmd.equals("edit"))
...
#end
The next operation to handle is Deleting an account.
...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
#if ($cmd.equals("add") || $cmd.equals("update"))
...
#elseif ($cmd.equals("delete"))
#if ($classPK > 0)
#set ($V = $expandoRowLocalService.deleteRow($accountsTableName, $accountsTableName, $classPK))
Account deleted!
#set ($classPK = 0)
#end
#elseif ($cmd.equals("edit"))
...
#end
That's it... Pretty simple? Sure is! Next, the Edit operation.
...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
#if ($cmd.equals("add") || $cmd.equals("update"))
...
#elseif ($cmd.equals("delete"))
...
#elseif ($cmd.equals("edit"))
Editting...
#if ($classPK > 0)
#set ($firstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $classPK, ""))
#set ($lastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $classPK, ""))
#set ($balance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $classPK, 0.0))
#end
#end
So, we retrieved the data stored in the requested account and put them into some placeholder variables.
Finally, we're ready to display some UI elements. We'll have two different views, a table listing the accounts, and a form for adding/editing accounts.
...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>
#if (!$cmd.equals("edit"))
...
#else
...
#end
Ok, so when we show the table we need some frillies like a "Create Account" button and some column headers.
...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>
#if (!$cmd.equals("edit"))
<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
<br /><br />
<table class="lfr-table">
<tr>
<th>Account Number</th>
<th>First Name</th>
<th>Last Name</th>
<th>Balance</th>
<th>Modified Date</th>
<th><!----></th>
</tr>
#else
...
#end
Here we're going to add the calls to get the number of, and list of all the existing accounts.
...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>
#if (!$cmd.equals("edit"))
<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
<br /><br />
<table class="lfr-table">
<tr>
<th>Account Number</th>
<th>First Name</th>
<th>Last Name</th>
<th>Balance</th>
<th>Modified Date</th>
<th><!----></th>
</tr>
#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))
#else
...
#end
Iterate through the list.
...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>
#if (!$cmd.equals("edit"))
<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
<br /><br />
<table class="lfr-table">
<tr>
<th>Account Number</th>
<th>First Name</th>
<th>Last Name</th>
<th>Balance</th>
<th>Modified Date</th>
<th><!----></th>
</tr>
#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))
#foreach($row in $rows)
#set ($currentClassPK = $row.getClassPK())
...
#end
#if ($rowsCount <= 0)
<tr>
<td colspan="5">No Accounts were found.</td>
</tr>
#end
</table>
# of Accounts: ${rowsCount}
#else
...
#end
Let's draw each table row (the accounts).
...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>
#if (!$cmd.equals("edit"))
<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
<br /><br />
<table class="lfr-table">
<tr>
<th>Account Number</th>
<th>First Name</th>
<th>Last Name</th>
<th>Balance</th>
<th>Modified Date</th>
<th><!----></th>
</tr>
#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))
#foreach($row in $rows)
#set ($currentClassPK = $row.getClassPK())
<tr>
<td>${currentClassPK}</td>
#set ($currentFirstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $currentClassPK, ""))
<td>${currentFirstName}</td>
#set ($currentLastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $currentClassPK, ""))
<td>${currentLastName}</td>
#set ($currentBalance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $currentClassPK, 0.0))
<td align="right">${numberTool.currency($currentBalance)}</td>
#set ($currentModifiedDate = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "modifiedDate", $currentClassPK, $dateTool.getDate()))
<td>${dateFormatDateTime.format($currentModifiedDate)}</td>
<td>
<a href="${renderUrl}&${namespace}cmd=edit&${namespace}classPK=${currentClassPK}">Edit</a> |
<a href="${renderUrl}&${namespace}cmd=delete&${namespace}classPK=${currentClassPK}">Delete</a>
</td>
</tr>
#end
#if ($rowsCount <= 0)
<tr>
<td colspan="5">No Accounts were found.</td>
</tr>
#end
</table>
# of Accounts: ${rowsCount}
#else
...
#end
Well, that was a mouthfull, but it should be pretty familliar design pattern. The general theme here is "ease of use". It didn't take much time or anything too tricky to get at the data.
The final piece of code is of course the input form.
...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>
#if (!$cmd.equals("edit"))
...
#else
<form action="$renderUrl" method="post" name="${namespace}fm10">
<input type="hidden" name="${namespace}classPK" value="${classPK}" />
<input type="hidden" name="${namespace}cmd"
#if ($classPK > 0)
value="update"
#else
value="add"
#end
/>
<table class="lfr-table">
<tr>
<td>First Name:</td>
<td>
<input type="text" name="${namespace}firstName" value="${firstName}" />
</td>
</tr>
<tr>
<td>Last Name:</td>
<td>
<input type="text" name="${namespace}lastName" value="${lastName}" />
</td>
</tr>
<tr>
<td>Balance:</td>
<td>
<input type="text" name="${namespace}balance" value="${numberTool.format($balance)}" />
</td>
</tr>
</table>
<br />
<input type="submit" value="Save" />
<input type="button" value="Cancel" onclick="self.location = '${renderUrl}'" />
</form>
#end
Two significant items to note are ${renderUrl} and ${namespace}. Remember that we're running within the context of a portlet, which means that we have to get a base url from the portal (we can't just use any old url). The code we wrote earlier got us an url from the request. Secondly, we need to namespace any parameter we're going to post back to that url so that the container knows to which portlet it belongs (there might be more that one portlet on the page).
Now you have a result which should look something like this:
Creating/Editting an account:
List with 4 accounts:
Here's the whole template with comments.
#set ($locale = $localeUtil.fromLanguageId($request.get("locale")))
#set ($dateFormatDateTime = $dateFormats.getDateTime($locale))
<h1>First Expando Bank</h1>
##
## Define the "name" for our ExpandoTable.
##
#set ($accountsTableName = "AccountsTable")
##
## Get/Create the ExpandoTable to hold our data.
##
#set ($accountsTable = $expandoTableLocalService.getTable($accountsTableName, $accountsTableName))
#if (!$accountsTable)
#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))
#set ($accountsTableId = $accountsTable.getTableId())
##
## Create an ExpandoColumn for each field in the form.
##
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "firstName", 15)) ## STRING
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "lastName", 15)) ## STRING
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "balance", 5)) ## DOUBLE
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "modifiedDate", 3)) ## DATE
#end
##
## Do some request handling setup.
##
#set ($renderUrl = $request.get("render-url"))
#set ($namespace = $request.get("portlet-namespace"))
#set ($cmd = $request.get("parameters").get("cmd"))
#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)
##
## Check to see if a classPK was passed in the request.
##
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))
##
## Check if we have received a form submission?
##
#if ($cmd.equals("add") || $cmd.equals("update"))
##
## Let's get the form values from the request.
##
#set ($firstName = $request.get("parameters").get("firstName"))
#set ($lastName = $request.get("parameters").get("lastName"))
#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
#set ($date = $dateTool.getDate())
##
## Validate the params to see if we should proceed.
##
#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
##
## Check to see if it's a new Account.
##
#if ($classPK <= 0)
#set ($classPK = $dateTool.getDate().getTime())
#end
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))
##
## Show a response.
##
#if ($cmd.equals("update"))
Thank you, ${firstName}, for updating your account with our bank!
#else
Thank you, ${firstName}, for creating an account with our bank!
#end
#else
Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
#end
#set ($classPK = 0)
#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)
#elseif ($cmd.equals("delete"))
##
## Delete the specified Row.
##
#if ($classPK > 0)
#set ($V = $expandoRowLocalService.deleteRow($accountsTableName, $accountsTableName, $classPK))
Account deleted!
#set ($classPK = 0)
#end
#elseif ($cmd.equals("edit"))
##
## Edit the specified Row.
##
Editting...
#if ($classPK > 0)
##
## Get the account specific values
##
#set ($firstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $classPK, ""))
#set ($lastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $classPK, ""))
#set ($balance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $classPK, 0.0))
#end
#end
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>
#if (!$cmd.equals("edit"))
##
## Now we're into the display logic.
##
<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
<br /><br />
<table class="lfr-table">
<tr>
<th>Account Number</th>
<th>First Name</th>
<th>Last Name</th>
<th>Balance</th>
<th>Modified Date</th>
<th><!----></th>
</tr>
##
## Get all the current records in our ExpandoTable. We can paginate by passing a
## "begin" and "end" params.
##
#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))
#foreach($row in $rows)
##
## Get the classPK of this row.
##
#set ($currentClassPK = $row.getClassPK())
<tr>
<td>${currentClassPK}</td>
#set ($currentFirstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $currentClassPK, ""))
<td>${currentFirstName}</td>
#set ($currentLastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $currentClassPK, ""))
<td>${currentLastName}</td>
#set ($currentBalance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $currentClassPK, 0.0))
<td align="right">${numberTool.currency($currentBalance)}</td>
#set ($currentModifiedDate = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "modifiedDate", $currentClassPK, $dateTool.getDate()))
<td>${dateFormatDateTime.format($currentModifiedDate)}</td>
<td>
<a href="${renderUrl}&${namespace}cmd=edit&${namespace}classPK=${currentClassPK}">Edit</a> |
<a href="${renderUrl}&${namespace}cmd=delete&${namespace}classPK=${currentClassPK}">Delete</a>
</td>
</tr>
#end
#if ($rowsCount <= 0)
<tr>
<td colspan="5">No Accounts were found.</td>
</tr>
#end
</table>
# of Accounts: ${rowsCount}
#else
##
## Here we have our input form.
##
<form action="$renderUrl" method="post" name="${namespace}fm10">
<input type="hidden" name="${namespace}classPK" value="${classPK}" />
<input type="hidden" name="${namespace}cmd"
#if ($classPK > 0)
value="update"
#else
value="add"
#end
/>
<table class="lfr-table">
<tr>
<td>First Name:</td>
<td>
<input type="text" name="${namespace}firstName" value="${firstName}" />
</td>
</tr>
<tr>
<td>Last Name:</td>
<td>
<input type="text" name="${namespace}lastName" value="${lastName}" />
</td>
</tr>
<tr>
<td>Balance:</td>
<td>
<input type="text" name="${namespace}balance" value="${numberTool.format($balance)}" />
</td>
</tr>
</table>
<br />
<input type="submit" value="Save" />
<input type="button" value="Cancel" onclick="self.location = '${renderUrl}'" />
</form>
#end
<br /><br />

