
Extend Liferay Tables
Introduction#
In the following sections you will be taken step by step from nothing to a fully working user add that takes in your new field(s).
One of the most common needs of users of Liferay is to extend certain functionality for their needs. This article will cover extending the user table to accept their hometown newspaper. Clearly this is just an example, and could be changed to include the name of their spouse, dog, cat, or whatever is wanted.
Overview
The idea here is to create a new table which has your custom field(s) for the user table. If you are adding hometown newspaper for example, you would create a new table in the Liferay database that has a unique id column, your hometown newspaper column, and a reference to the user that is associated with this data.
You can do this by running our service builder to create the extended user class, and then in the LocalServiceImpl file you would add the data you needed to the extra table.
Details#
In the following sections you will be taken step by step from nothing to a fully working user add that takes in your new field(s).
Service Builder#
Step one is to create a service.xml file with your new table information in in. You will save this file as /ext/ext-impl/service.xml.
This is the file we used for the hometown newspaper addition:
<?xml version="1.0"?> <!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 5.1.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_5_1_0.dtd"> <service-builder root-dir=".." package-path="com.ext.portlet"> <portlet name="AddUserEXT" short-name="AddUserEXT" /> <!-- This is the folder that will be created under ext-impl --> <entity name="AddUserExtras" local-service="true"> <!-- This is the table that will be used, and the class that will be generated --> <!-- PK fields --> <column name="entryId" type="String" primary="true" /> <!-- Other fields --> <column name="HomeTownNewspaper" type="String" /> </entity> </service-builder>
You now need to run "ant build-service" from the "ext/ext-impl/" folder.
More information about service.xml creation here
AddUserExtrasLocalServiceImpl #
Now that the service builder has run, the following file should have been created (along with others): ext/ext-impl/src/com/ext/portlet/AddUserEXT/service/impl/AddUserExtrasLocalServiceImpl.java
Go ahead and open that file. We now need to add the addUser function to our class. After your edition, it should look something like this (depending on the liferay version you are using):
import com.ext.portlet.AddUserEXT.service.AddUserExtrasLocalService; import com.ext.portlet.AddUserEXT.model.AddUserExtras; //^ the AddUserExtras is not available in programming time. Only the ./impl/AddUserExtras.java is. // however you should use the interface file AddUserExtras. import com.ext.portlet.AddUserEXT.service.persistence.AddUserExtrasUtil; [...] import com.liferay.portal.model.User; import com.liferay.portal.service.UserServiceUtil; import com.liferay.portal.PortalException; import com.liferay.portal.SystemException; import java.util.Locale; [...] public AddUserExtras addUser( java.lang.String companyId, boolean autoUserId, java.lang.String userId, boolean autoPassword, java.lang.String password1, java.lang.String password2, boolean passwordReset, java.lang.String emailAddress, java.util.Locale locale, java.lang.String firstName, java.lang.String middleName, java.lang.String lastName, java.lang.String nickName, java.lang.String prefixId, java.lang.String suffixId, boolean male, int birthdayMonth, int birthdayDay, int birthdayYear, java.lang.String jobTitle, java.lang.String organizationId, java.lang.String locationId, java.lang.String dogName) throws PortalException, SystemException { // Note this call is dependent upon your version of liferay. Ensure the signature matches the API in // portal-impl/src/com/liferay/portal/service/UserLocalServiceUtil.java User user = null; try{ user = UserServiceUtil.addUser( companyId, autoUserId, userId, autoPassword, password1, password2, passwordReset, emailAddress, locale, firstName, middleName, lastName, nickName, prefixId, suffixId, male, birthdayMonth, birthdayDay, birthdayYear, jobTitle, organizationId, locationId ); }catch(Exception e){ throw new PortalException(e.getMessage()); } // We now need to add the user to our new table // By default liferay uses the e-mail address as a primary key, so I will too. AddUserExtras userInfo = AddUserExtrasUtil.create(email); userInfo.setHomeTownNewspaper(newspaper); return AddUserExtrasUtil.update(userInfo); } }
Now that we have added addUser to our new class, when our class is called it will add a user to the liferay user table as well as our extra fields to our new table, and they will be linked via the e-mail address.
When making changes in the extension environment, the only files within the "impl" folder that you should touch are the Impl classes. Everything else is generated for you. Now that we've added a method to AddUserExtrasLocalServiceImpl.java, by simply running build-service on your service.xml, it will propagate that method signature to all wrapping classes, including your ServiceUtil.java classes.
Mapping the UI to the Service Layer #
In order to call your newly created addUser() method, we need to map the front-end to call our back-end. This can be done in two ways, depending on your need. One option is to simply override our struts action-mapping. The other option is to simply create your own struts action-mapping.
Overriding our struts action-mapping #
Since we know that within the core source, /my_account/create_account maps to AddUserAction which then calls UserServiceUtil.addUser(), we want to make modifications such that it calls our newly created addUser() method instead. This can be easily done by adding another action-mapping within /ext-web/docroot/WEB-INF/struts-config.xml. Since action-mappings are loaded sequentially, and since the struts-config.xml of the extension environment is loaded AFTER the struts-config.xml of the portal core, we know that any action-mappings in the extension environment that have the same path name will override those action-mappings in the portal core. Hence, we can then do something like this:
<action path="/my_account/create_account" type="com.ext.portlet.adduserextras.action.CreateUserAction"> <forward name="portlet.my_account.create_account" path="portlet.my_account.create_account" /> <forward name="portlet.my_account.view" path="portlet.my_account.view" /> </action>
This will effectively override the portal core's struts action-mapping to call our specified action class instead of Liferay's. Moreover, no changes to the JSP are necessary since the form is still going to the same struts action-mapping.
Creating a new struts action-mapping #
Alternatively, we can just create our own new struts action-mapping. This sometimes makes sense when you don't want to remove any of our functionality (i.e. you may need to use our struts action-mapping for something else). Add the following to ext/ext-web/docroot/WEB-INF/struts-config.xml:
<action path="/AddUserExtras/create_account" type="com.ext.portlet.adduserextras.action.CreateUserAction"> <forward name="portlet.my_account.create_account" path="portlet.my_account.create_account" /> <forward name="portlet.my_account.view" path="portlet.my_account.view" /> </action>
We now need to edit Liferay to call our addUser instead of the default Liferay addUser.
You need to create the following file:
ext/ext-web/docroot/html/portlet/my_account/create_account.jsp
The easiest way to populate this file is to grab the same file from the portal source, that's in:
portal/portal-web/docroot/html/portlet/my_account/create_account.jsp
This file contains the form that is used when the new account information is being gathered. You'll want to add your input fields to the form to gather and capture the new fields that your user info addition object needs. The file is pretty simple, just follow the existing example here and you should be fine.
Now that you have the file copied, change the struts to point to our new file instead of the old liferay one, just in the case you have not overwritten the struts-config.xml file!
Change this line:
<form action="<portlet:actionURL> <portlet:param name="struts_action" value="/**my_account/create_account**" /> </portlet:actionURL>" method="post" name="<portlet:namespace />fm" onSubmit="<portlet:namespace />createAccount(); return false;">
to this:
<form action="<portlet:actionURL> <portlet:param name="struts_action" value="/**AddUserExtras/create_account**" /> </portlet:actionURL>" method="post" name="<portlet:namespace />fm" onSubmit="<portlet:namespace />createAccount(); return false;">
Don't forget to add your new field to this page so people can enter their hometown newspaper!
For this, you can add a new AddUserExtras variable to your new create_account.jsp under User and Contact variables:
User user2 = null; Contact contact2 = null; **AddUserExtras userInfo2 = null;**
Then add your hometown newspaper input box wherever you see fit:
<tr> <td> <%= LanguageUtil.get(pageContext, "HomeTownNewspaper") %> </td> <td style="padding-left: 10px;"></td> <td> <liferay-ui:input-field model="<%= AddUserExtras.class %>" bean="<%= userInfo2 %>" field="HomeTownNewspaper" /> </td> </tr>
In order to access to the new class AddUserExtras from the JSP file, you will need to edit the file .../hml/common/init-ext.jsp in order to import the model file. The simpliest way to do it is to copy the .../hml/common/init-ext.jsp file from the portal source code to the extention environment and add it the import code. The init-ext.jsp must look like:
<%@ page import="com.ext.portlet.adduserEXT.model.AddUserExtras" %>
Remember that the model file is not available at programming time, only the implementation file is available, because AddUserExtras is an interface to the AdduserExtrasImpl.java.
Creating an actionclass to call your new Impl file #
You are now going to create the following file:
ext/ext-impl/src/com/ext/portlet/adduserextras/action/CreateUserAction.java
And make it look something like this: (Please make sure to check all of the parameter names to be consistent with the names appear in 'create_account.jsp' ('first_name' may have become 'firstName') since there has been some changes on input names throughout version changes in liferay core codes! In fact, you are encouraged to check 'com.liferay.portlet.myaccount.action.AddUserAction' to see if there are any changes in create_user implementation.)
package com.ext.portlet.adduserextras.action; public class CreateUserAction extends PortletAction { public void processAction(ActionMapping mapping, ActionForm form, PortletConfig config, ActionRequest req, ActionResponse res) throws Exception { // declarations String firstName = req.getParameter("first_name"); String lastName = req.getParameter("last_name"); String address1 = req.getParameter("address1"); String address2 = req.getParameter("address2"); String newspaper = req.getParameter("Hometown Newspaper"); String emailAddress = req.getParameter("email_address"); String userId = req.getParameter("user_id"); String password1 = req.getParameter("password_1"); String password2 = req.getParameter("password_2"); try { AddUserExtrasLocalServiceUtil.addUser( firstName, lastName, emailAddress, userId, password1, password2, newspaper); req.setAttribute("success", "true"); setForward(req, "portlet.my_account.view"); } catch (Exception e) { if (e != null && e instanceof CaptchaException || e instanceof DuplicateUserEmailAddressException || e instanceof DuplicateUserIdException || e instanceof ReservedUserEmailAddressException || e instanceof ReservedUserIdException || e instanceof UserEmailAddressException || e instanceof UserFirstNameException || e instanceof UserIdException || e instanceof UserLastNameException || { SessionErrors.add(req, e.getClass().getName()); setForward(req, "portlet.my_account.create_account"); } else if (e != null && e instanceof UserPasswordException) { UserPasswordException upe = (UserPasswordException)e; SessionErrors.add(req, e.getClass().getName(), upe); setForward(req, "portlet.my_account.create_account"); } else if (e != null && e instanceof PrincipalException) { SessionErrors.add(req, e.getClass().getName()); setForward(req, "portlet.my_account.create_account"); } else { req.setAttribute(PageContext.EXCEPTION, e); setForward(req, "portlet.my_account.create_account"); } }
Database #
Finally, after running build-service on your service.xml, you should note that a corresponding table should exist within portal-tables.sql that matches your entity name. You will need to create this table within your database, as it will hold the new fields you specified. The portal-tables.sql file is a template file - running "ant build-db" within ext/sql/ with create the database specific files within ext/sql/create/ and ext/sql/portal/.
Liferay can create the database for you if none exists. In order to make this work with your new tables you need to copy the the following files from the sql folder to ext-impl/classes/com/liferay/portal/tools/sql/dependencies:
- portal-data-common.sql
- portal-data-counter.sql
- portal-data-release.sql
- portal-data-sample.vm
- portal-tables.sql
- indexes.sql
- quartz-tables.sql
- sequences.sql
- update-.sql
Here is a ANT target you can add to build.xml
<target name="copy-dependencies"> <copy todir="${ext-impl.classes.dir}/com/liferay/portal/tools/sql/dependencies"> <fileset dir="${project.dir}/sql" includes="portal-data-common.sql,portal-data-counter.sql,portal-data-release.sql,portal-data-sample.vm,portal-tables.sql,indexes.sql,quartz-tables.sql,sequences.sql,update-*.sql" /> </copy> </target>
Conclusion#
Rather than assuming that you are replacing Liferay's classes, the better way to think of it is that you are extending Liferay's classes.