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

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

See the follow-up article here.

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

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

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

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

 

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

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

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

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

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

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

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

Step 2) Create our Template

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

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

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

<h1>First Expando Bank</h1>

Step 3) We create our Article.

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

The result should be something like this:

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

Give our table a name.

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

#set ($accountsTableName = "AccountsTable")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Before we can continue we need to do some cleanup.

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

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

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

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

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

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

The next operation to handle is Deleting an account.

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

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

Account deleted!

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

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

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

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

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

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

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

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

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

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

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

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

<br /><br />

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

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

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

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

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

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

Iterate through the list.

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

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

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

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

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

...
#end

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

</table>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<br />

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

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

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

Creating/Editting an account:

List with 4 accounts:

Here's the whole template with comments.

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

<h1>First Expando Bank</h1>

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

#set ($accountsTableName = "AccountsTable")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		Account deleted!

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

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

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

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

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

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

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

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

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

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

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

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

	</table>

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

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

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

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

<br /><br />
Blogs
Nice feature!
In (since) which version is that available?

Greats
Tobias
It's currently in trunk, and should be in 5.0.2.
Ray, I would update the title of the blog post to something like (Liferay Portal 5.0.2) because currently you can not use Expandos with any LRP released and the first experience from a developer trying it could be that it does not work. 5.0.2 is around the corner but ...

Really nice article in any case and a very useful feature,
Great work Ray, thanks! I was searching for a solution extend dynamically Liferay tables and I found your solution. You show an example with new table, column, etc. But what about adding an expando column/value to an existing table? Is it possible? I tried to add an expando column 'foo' to user_ table and store value 'bar' for the specified user as the following way:

User user = UserLocalServiceUtil.getUserByEmailAddress(...);

ExpandoTable table = ExpandoTableLocalServiceUtil.addTable(PortalUtil.getClassNameId(user.getClass().getName()), "user_");
if(table!=null) {
ExpandoColumn column = ExpandoColumnLocalServiceUtil.getColumn(table.getTableId(), "foo");
if(column!=null){
ExpandoValueLocalServiceUtil.addValue(column.getColumnId(), user.getUSerId(), user.getUserId(), "bar");
}
}

Is this correct? I'm afraid the second parameter of addValue (rowid) isn't correct, but what should I write there? And what about the third parameter (classPK), is that right? classPK is the primary key of the user?
If my code/idea is correct, I would like to ask you about deleting a "parent" record. I'll try to explain what I think :-) I put the value 'bar' into expando table for the user Joe Blogs. When I delete Joe Blogs from Liferay the 'bar' value from expando tables will not be deleted. Is this right? Should I delete manually expando records? There isn't a way to do this automatically? Do you have any idea? Or I misunderstood the whole expando-thing? :-)

Thanks,
Gabor
Hello Gabor,

The API has changed slightly. Have a look at:

http://lportal.svn.sourceforge.net/svnroot/lportal/portal/trunk/portal-impl/src/com/liferay/portlet/expando/service/impl/ExpandoValueLocalServiceImpl.java

you'll notice that the final API is a little more intuitive and easier to work with. Also, the final (stable) API will officially be available in the upcomming 5.0.2 release, due out next week. For example, you don't need to worry about rowIds as these are more for internal management than external (though it is available for use in iterating over table rows...).

Also, at least for users, any ExpandoValue associated directly with the User object and referenced by User's primary key will be deleted when the user is deleted, regardless of what table it's in... so you can have 100 tables associated with the User class and when a user is deleted... all the values associated with that user, in every table, go with it.

HTH!
Thank you Ray!

It sounds good! We waiting for 5.0.2 :-)
However, I found a solution for delete problem. I added this line to portal/ext.properties:
value.object.listener.com.liferay.portal.model.User=com.liferay.portal.model.UserListener,com.liferay.portal.model.MyUserListener

Then I created a new class MyUserListener and in onAfterRemove method:
User user = (User)model;
ExpandoValueLocalServiceUtil.deleteValues(PortalUtil.getClassNameId(user.getClass().getName()), user.getUserId());

This removes all records belonging to the deleted user.

But, I'll take a look at the new API and rewrite my code with the official solution.
Thanks for the post.

I initially approached this as an "oh carp, yet another thing to learn about Liferay". Well, I'm glad I read through your post. Journal templates are fantastic!

There was some initial confusion about default variables. I discovered the JT VM instances get initialized differently than regular portlet VM instances. For example, getting the user id was a bit difficult and I finally pulled it from the request tree. It would probably be beneficial to provide privileged based access to services instead of using hard filters in the boot properties file. Nonetheless, it was all a worthwhile experience that exposed some great functionality!

Is it possible to create reusable VM libraries within the JT paradigm?
It is possible in Journal to create reusable VM libraries. You simply place your library code in standalone JTs (JT's not associated with any JStructure). Then you can refer to these "library templates" directly from others using the VM #parse method. The only catch is that you must prepend the name of the Template with "$journalTemplatesPath".

e.g.
#parse ("$journalTemplatesPath/MY_VM_LIB")
Thanks for this, Ray! I have just gotten around to playing with this functionality, and it's awesome. Question, though: is it possible in the current model to return only rows that have been created by the signed-in user? I am looking at:
public java.util.List<com.liferay.portlet.expando.model.ExpandoRow> getRows(
long tableId, int start, int end)
throws com.liferay.portal.SystemException;

and don't think I see a way to extract that data. I have modified your code slightly to add the current user ID as a column.

Thanks!
Hi Ray!
I am using UserGroupLocalService in VM file.
I want to get userGroupId from the usergroup table which in database, but in UserGroupLocalService does'nt have any method for getting userGroupId.
$userGroupLocalService.getUserUserGroups()
how will i get?
please help me.
thanks
I'm trying to use this feature to display data that is saved to the database through a "Request Form".

I retrieved the generated table name (109_INSTANCE_98OX_201, basically the instance-id of the portlet) from the EXPANDOTABLE table, but when trying to access the table it fails.

Basically the

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

does not work even though $accountsTableName contains the correct table name

I veried that the feature itself was working by creating a table manually through the API.

What am I missing?
Try:

#set ($accountsTable = $expandoTableLocalService.getTable("com.liferay.portlet.webform.util.WebFormUtil", $accountsTableName))
Hi Ray, great!

On question, do the attribute name and value support locale?

It was notified that portlet 139 Expando was applied on users and organizations. Logically, it should be possible to apply portlet 139 Expando on user groups, communities, roles, etc. Is it right?

Thanks

Jonas Yuan
If there exists a localization for the attribute name it will be used. Thus you can localize the names by adding the key to the locale files. The values themselves are not localized.

Custom attributes can be applied to any entity generated via service builder, as such the Expando portlet can be used to manage those. No changes are required to the portlet, juswt create a portletURL as you find in the User and Organization management.
Hi Ray,

Thank you! How about custom types in custom attributes? That is, adding custom types (Image Gallery images and Document Library documents) in custom attributes (Expando).

http://issues.liferay.com/browse/LPS-2087

Any comments or suggestions?
Why don't you just store a reference to one of those types?

e.g. a custom attribute called "favorite-pic" of type string stores an url to the image, or of type long, stores the primaryKey of the image, etc... All you need to provide is the selection and rendering logic. Both very easy to implement based on existing examples.
Hi, I tried this again with 5.2.1, and it does not seem to work any longer.

I have a webform and data is submitted. So I looked up the generated table in the database (EXPANDOTABLE). The entry there gave me the tableanme: 1_WAR_webformportlet_INSTANCE_y0Ik_2

But the following code does not return a rowcount:

#set ($tableName = "1_WAR_webformportlet_INSTANCE_y0Ik_2")
#set ($tableType = "com.liferay.portlet.webform.util.WebFormUtil")
#set ($rowCount = $expandoRowLocalService.getRowsCount($tableType, $tableName))

what am I missing here?
I found my error. Apparently the table type has change from

com.liferay.portlet.webform.util.WebFormUtil

to

com.liferay.webform.util.WebFormUtil

Now it's working again
Hi Ray,

I read this blog, and create new table user in expando it has 2 coulumn userid and points,
but i want to map userid of this table to userID of user table.
when we delete any user in liferay, same user is also deleted in expando user table.
how will i do
please reply
When you set the className of the table to 'com.liferay.portal.model.User' and the primaryKey to that of a given user, the data will be deleted when the user is deleted automatically.
HI RAY,

iam trying expandos thing in portal 5.1.1 but it is not performing well.
there is no error in code, i have copied the template as specifed.
when i click save the values are not saving. and there is no error in logs also .
what can be the problem?
After getting it to work with 5.2 I have a problem with caching.

I can see four entries in the database but the template only shows 3 of them.

The "Cacheable" attribute is disabled for my VM template, but I even restarted Liferay but still can't see the new row.

Is there another level of caching? But after restarting the server all those caches should be empty, right?
I'll have to test this.

Can you file a bug and provide a minimal test case? Thanks!
Thanks for the answer.

It's hard do create a test case (because some rows are show) but I'll try. Is there anything I can turn on in Liferay to debug that in my environment? e.g. log messages to find out what's going on?
This is very cool.. we are hoping to use it to replace some of our sharepoint lists... my problem is when I copy the template you have and try to use it in a content it doesn't show.. all my other do... am I missing a step? (Using 5.1.2)
nevermind I missed the structure.... Heres the rub now... It renders but when I hit the create button it thinks and the does nothing...
hi ray,
why it didnt works on my office's computer(linux) but perfectly works on my personal computer(window) and my friend's computer(linux).

the table name,column name perfectly created but i cant insert any value.
hmm... Which version of the portal. We had a small API change in a latter version of 5.1 (I know API changes are bad... but it was a sever limitation which incurred the change, I suspect that's the problem).
Apparently (and logically), Users' and Organizations' Custom Attributes are implemented with Expandos. For a Project, we need custom attributes on other entities such as events and communities as well.

I've already managed to create an expandotable that refers to f.e. the CalEvent class and added fields to it. Now I want to include this extra fields in the form of the Create Event page. I found the <liferay-ui:custom-attribute-list /> JSP tag, but, f.e. for CalEvent, where do I put this tag, and will the service layer know what to do with the "extra" inputfields?
The service layer will automatically know what to do with the input fields as far as collecting them and passing them along to the service implementation layer... But unfortunately, it appears that the operation to persist them was not called in the add/update methods, specifically w.r.t. CalEvent.

What version are you using? It's simple to patch, add these two lines later in the add/update methods:

ExpandoBridge expandoBridge = event.getExpandoBridge();
expandoBridge.setAttributes(serviceContext);
Thanks alot for your quick response. I'm using 5.2.1.

I tried using <liferay-ui:custom-attribute-list ... /> in edit_event.jsp and it showed the expando fields, awesome! Values are not yet persisted like you said. But where exactly should I put your lines of code? I'm pretty new to Liferay development...

Thanks in advance! Very exciting this Expando stuff
I'd have to say just before the comment:

// Social

But I've already committed the fix to the CalEvent service here

http://issues.liferay.com/browse/LPS-2317

and requested for backport to 5.2.x.
This is exactly what I'm looking for!

I get "No ExpandoColumn exists with the primary key 0" when I use ExpandoColumnLocalServiceUtil.addColumn(...) and (Since I am not logged in during the startup hook) "PermissionChecker not initialized" when I use ExpandoColumnServiceUtil.addColumn(...)

Is there a way to create Custom Attributes during the startup hook?

Here is the code:

//in startupaction.java
addAttribute(myUser, "myStringAttribute", 15, "default");


//in myUtilClass.java
public static void addAttribute(User liferayUser, String name, int type, Serializable defaultValue)
throws PortalException {
//String className = liferayUser.getClass().getName();
String className = com.liferay.portal.model.User.class.getName();
try {
ExpandoTable table = null;
try {
table = ExpandoTableLocalServiceUtil.getDefaultTable(
className);
}
catch (NoSuchTableException nste) {
table = ExpandoTableLocalServiceUtil.addDefaultTable(
className);
}
ExpandoColumnLocalServiceUtil.addColumn(
table.getTableId(), name, type, defaultValue);
}
catch (Exception e) {
if (e instanceof PortalException) {
throw (PortalException)e;
}
else {
}
}
}
To start, which version of the portal are you using?
Great! You can reduce all that code by alot using the convenience API called ExpandoBridge.

liferayUser.getExpandoBridge().addAttribute(name, type, defaultValue);

If you don't have a user at that time, use:

ExpandoBridge eb = new ExpandoBridgeImpl(User.class.getName(), 0);
eb.addAttribute(name, type, defaultValue);

Later to get/set those values for any user simply:

Object attr = someUser.getExpandoBridge().getAttribute(name);

someUser.getExpandoBridge().setAttribute(name, someValue);
Many thanks for your answer Ray!

But I still got the problem with the PermissionChecker not initialized.

After I create the appropriate role and user in the startupaction.java (on application.startup.events) i try the:

liferayUser.getExpandoBridge().addAttribute(name, type, defaultValue);

Which gives the PrincipalException: PermissionChecker not initialized.
Also the:
ExpandoBridge eb = new ExpandoBridgeImpl(User.class.getName(), 0);
eb.addAttribute(name, type, defaultValue); <------------- gives the same exception.

Is it possible to create custom attributes in the startupaction hook?
It should be! We're doing that very thing in the project I'm currently tasked on.
Hi! I meet same problem in Liferay 5.2.3 - I'm trying to add new attribute (in my case for BlogsEntry) in portlet StartUpAction (configured via hooks-propeties).
I'm doing very similar:
ExpandoBridgeImpl blogExpandoBridge = new ExpandoBridgeImpl(BlogsEntry.class.getName());
// shareWith
if (blogExpandoBridge.hasAttribute("shareWith")) {
_log.debug("attribute shareWith already exists");
}
else {
_log.info("adding shareWith attribute");
blogExpandoBridge.addAttribute("shareWith");
}

But got PermissionChecker is not initialized exception. I think it happens, because during adding new column Liferay tried to identify - is current user allowed to add expando columns, but, since this code exeucted in StartUpAction - no current user and as result - no any PermissionChecker.

Any solution for this?

BTW - Expando looks like a great solution which should resolve many problems for extending default liferay objects!
Probably, problem in fact, Expando has services, but has no LocalServices - as far as I understand - one of the biggest difference - LocalServices did not do permission checking.

So, ExpandoServices always tried to do permission-checks, and in case they are called in (for example) startup action, since we do not have any user in context, permission checker is not initialized.

Having Expando LocalServices will allow to perform Expando operations without permission checking (supposing all permissions are already checked on top-level)
uups - sorry! Exando has local services as well, and, as I expected - their usage allow o perform exapndo attributes operations without checking permissions (for example in startup event).
Hi Alexey,

I'm also trying to add an attribute in a Startup hook (Liferay 5.2.3) like in your example and I also got this PermissionChecker not initialized exception.

I'm not really getting your later comments with the Local service. Have you been able to fix your problem using local service? If so, would you mind posting your code here?

Thanks a lot!
Hi Ray, brilliant blog thread!

I have a simple question regarding posting data using the "web form" portlet and reading the entries using the velocity template approach.

My Web Form portlet instance works well and I can see all of the data in the Expando tables. But I'm trying to create a very simple way to read these values into a table with pagination using a velocity template.

Any ideas how I can accomplish this using the last portion of your code here?

Thanks!
-HarryO