Blogs
So I'm a long-time supporter of ServiceBuilder. I saw its purpose way back on Liferay 4 and 5 and have championed it in the forums and here in my blog.
With the release of Liferay 7, ServiceBuilder has undergone a few changes mostly related to the OSGi modularization. ServiceBuilder will now create two modules, one API module (comparable to the old service jar w/ the interfaces) and a service module (comparable to the service implementation that used to be part of a portlet).
But at it's core, it still does a lot of the same things. The service.xml file defines all of the entities, you "buildService" (in gradle speak) to rebuild the generated code, consumers still use the API module and your implementation is encapsualted in the service module. The generated code and the Liferay ServiceBuilder framework are built on top of Hibernate so all of the same Spring and Hibernate facets still apply. All of the features used in the past are also supported, including custom SQL, DynamicQuery, custom Finders and even External Database support.
External Database support is still included for ServiceBuilder, but there are some restrictions and setup requirements that are necessary to make them work under Liferay 7.
Examples are a good way to work through the process, so I'm going to present a simple ServiceBuilder component that will be tracking logins in an HSQL database separate from the Liferay database. That last part is obviously contrived since one would not want to go to HSQL for anything real, but you're free to substitute any supported DB for the platform you're targeting.
The Project
So I'll be using Gradle, JDK 1.8 and Liferay CE 7 GA2 for the project. Here's the command to create the project:
blade create -t servicebuilder -p com.liferay.example.servicebuilder.extdb sb-extdb
This will create a ServiceBuilder project with two modules:
- sb-extdb-api: The API module that consumers will depend on.
- sb-extdb-service: The service implementation module.
The Entity
So the first thing we need to define is our entity. The service.xml file is in the sb-extdb-service module, and here's what we'll start with:
<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd">
<service-builder package-path="com.liferay.example.servicebuilder.extdb">
<!-- Define a namespace for our example -->
<namespace>ExtDB</namespace>
<!-- Define an entity for tracking login information. -->
<entity name="UserLogin" uuid="false" local-service="true" remote-service="false" data-source="extDataSource" >
<!-- session-factory="extSessionFactory" tx-manager="extTransactionManager" -->
<!-- userId is our primary key. -->
<column name="userId" type="long" primary="true" />
<!-- We'll track the date of last login -->
<column name="lastLogin" type="Date" />
<!-- We'll track the total number of individual logins for the user -->
<column name="totalLogins" type="long" />
<!-- Let's also track the longest time between logins -->
<column name="longestTimeBetweenLogins" type="long" />
<!-- And we'll also track the shortest time between logins -->
<column name="shortestTimeBetweenLogins" type="long" />
</entity>
</service-builder>
This is a pretty simple entity for tracking user logins. The user id will be the primary key and we'll track dates, times between logins as well as the user's total logins.
Just as in previous versions of Liferay, we must specify the external data source for our entity/entities.
In our particular example we're going to be wiring up to HSQL, so I've taken the steps to create the HSQL script file with the table definition as:
CREATE MEMORY TABLE PUBLIC.EXTDB_USERLOGIN(
USERID BIGINT NOT NULL PRIMARY KEY,
LASTLOGIN TIMESTAMP,
TOTALLOGINS BIGINT,
LONGESTTIMEBETWEENLOGINS BIGINT,
SHORTESTTIMEBETWEENLOGINS BIGINT);
The Service
The next thing we need to do is build the services. In the sb-extdb-service directory, we'll need to build the services:
gradle buildService
Eventually we're going to build out our post login hook to manage this tracking, so we can guess that we could use a method to simplify the login tracking. Here's the method that we'll add to UserLoginLocalServiceImpl.java:
public class UserLoginLocalServiceImpl extends UserLoginLocalServiceBaseImpl {
private static final Log logger = LogFactoryUtil.getLog(UserLoginLocalServiceImpl.class);
/**
* updateUserLogin: Updates the user login record with the given info.
* @param userId User who logged in.
* @param loginDate Date when the user logged in.
*/
public void updateUserLogin(final long userId, final Date loginDate) {
UserLogin login;
// first try to get the existing record for the user
login = fetchUserLogin(userId);
if (login == null) {
// user has never logged in before, need a new record
if (logger.isDebugEnabled()) logger.debug("User " + userId + " has never logged in before.");
// create a new record
login = createUserLogin(userId);
// update the login date
login.setLastLogin(loginDate);
// initialize the values
login.setTotalLogins(1);
login.setShortestTimeBetweenLogins(Long.MAX_VALUE);
login.setLongestTimeBetweenLogins(0);
// add the login
addUserLogin(login);
} else {
// user has logged in before, just need to update record.
// increment the logins count
login.setTotalLogins(login.getTotalLogins() + 1);
// determine the duration time between the current and last login
long duration = loginDate.getTime() - login.getLastLogin().getTime();
// if this duration is longer than last, update the longest duration.
if (duration > login.getLongestTimeBetweenLogins()) {
login.setLongestTimeBetweenLogins(duration);
}
// if this duration is shorter than last, update the shortest duration.
if (duration < login.getShortestTimeBetweenLogins()) {
login.setShortestTimeBetweenLogins(duration);
}
// update the last login timestamp
login.setLastLogin(loginDate);
// update the record
updateUserLogin(login);
}
}
}
After adding the method, we'll need to build services again for the method to get into the API.
Defining The Data Source Beans
So we now need to define our data source beans for the external data source. We'll create an XML file, ext-db-spring.xml, in the sb-extdb-service/src/main/resources/META-INF/spring directory. When our module is loaded, the Spring files in this directory will get processed automatically into the module's Spring context.
<?xml version="1.0"?>
<beans
default-destroy-method="destroy"
default-init-method="afterPropertiesSet"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
>
<!--
NOTE: Current restriction in LR7's handling of external data sources requires us to redefine the
liferayDataSource bean in our spring configuration.
The following beans define a new liferayDataSource based on the jdbc.ext. prefix in portal-ext.properties.
-->
<bean class="com.liferay.portal.dao.jdbc.spring.DataSourceFactoryBean" id="liferayDataSourceImpl">
<property name="propertyPrefix" value="jdbc.ext." />
</bean>
<bean class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy" id="liferayDataSource">
<property name="targetDataSource" ref="liferayDataSourceImpl" />
</bean>
<!--
So our entities are all appropriately tagged with the extDataSource, we'll alias the above
liferayDataSource so it matches the entities.
-->
<alias alias="extDataSource" name="liferayDataSource" />
</beans>
These bean definitions are a big departure from the classic way of using an external data source. Previously we would define separate data source beans from the Liferay Data Source beans, but under Liferay 7 we must redefine the Liferay Data Source to point at our external data source.
This has a couple of important side effects:
The last line, the alias line, this line defines a Spring alias for the liferayDataSource as your named data source in your service.xml file.
So, back to our example. We're planning on writing our records into HSQL, so we need to add the properties to the portal-ext.properties for our external datasource connection:
# Connection details for the HSQL database
jdbc.ext.driverClassName=org.hsqldb.jdbc.JDBCDriver
jdbc.ext.url=jdbc:hsqldb:${liferay.home}/data/hypersonic/logins;hsqldb.write_delay=false
jdbc.ext.username=sa
jdbc.ext.password=
The Post Login Hook
So we'll use blade to create the post login hook. In the sb-extdb main directory, run blade to create the module:
blade create -p com.liferay.example.servicebuilder.extdb.event -t service -s com.liferay.portal.kernel.events.LifecycleAction sb-extdb-postlogin
Since blade doesn't know we're really adding a sub module, it has created a full standalone gradle project. While not shown here, I modified a number of the gradle project files to make the postlogin module a submodule of the project.
We'll create the com.liferay.example.servicebuilder.extdb.event.UserLoginTrackerAction with the following details:
/**
* class UserLoginTrackerAction: This is the post login hook to track user logins.
*
* @author dnebinger
*/
@Component(
immediate = true, property = {"key=login.events.post"},
service = LifecycleAction.class
)
public class UserLoginTrackerAction implements LifecycleAction {
private static final Log logger = LogFactoryUtil.getLog(UserLoginTrackerAction.class);
/**
* processLifecycleEvent: Invoked when the registered event is triggered.
* @param lifecycleEvent
* @throws ActionException
*/
@Override
public void processLifecycleEvent(LifecycleEvent lifecycleEvent) throws ActionException {
// okay, we need the user login for the event
User user = null;
try {
user = PortalUtil.getUser(lifecycleEvent.getRequest());
} catch (PortalException e) {
logger.error("Error accessing login user: " + e.getMessage(), e);
}
if (user == null) {
logger.warn("Could not find the logged in user, nothing to track.");
return;
}
// we have the user, let's invoke the service
getService().updateUserLogin(user.getUserId(), new Date());
// alternatively we could just use the local service util:
// UserLoginLocalServiceUtil.updateUserLogin(user.getUserId(), new Date());
}
/**
* getService: Returns the user tracker service instance.
* @return UserLoginLocalService The instance to use.
*/
public UserLoginLocalService getService() {
return _serviceTracker.getService();
}
// use the OSGi service tracker to get an instance of the service when available.
private ServiceTracker<UserLoginLocalService, UserLoginLocalService> _serviceTracker =
ServiceTrackerFactory.open(UserLoginLocalService.class);
}
Checkpoint: Testing
At this point we should be able to build and deploy the api module, the service module and the post login hook module. We'll use the gradle command:
gradle build
In each of the submodules you'll find a build/libs directory where the bundle jars are. Fire up your version of LR7CEGA2 (make sure the jdbc.ext properties are in portal-ext.properties file before starting) and put the jars in the $LIFERAY_HOME/deploy folder. Liferay will pick them up and deploy them.
Drop into the gogo shell and check your modules to ensure they are started.
Log into the portal a few times and you should be able to find the database in the data directory and browse the records to see what it contains.
Conclusion
Using external data sources with Liferay 7's ServiceBuilder is still supported. It's still a great tool for building a db-based OSGi module, still allows you to generate a bulk of the DB access code while encapsulating behind an API in a controlled manner.
We reviewed the new constraints on ServiceBuilder imposed by Liferay 7:
- Only one (external) data source per Service Builder module.
- The external data source objects, the tables, indexes, etc., must be manually managed.
- For a transaction to span multiple Service Builder modules, XA transactions must be used.
You can find the GitHub project code for this blog here: https://github.com/dnebing/sb-extdb

