Wiki Wiki

ZK integration with Liferay.

DLPortlet and DLLiferayService#

ZK did already a big step towards Liferay support with a Portlet class DHtmlLayoutPortlet. You can run any ZK page as a Liferay portlet without any further customization (see ZK Liferay Installation Guide). However, you are somehow limited when accessing Liferay Services.

Portlet and Servlet Context#

As you know, ZK as a server centric solution use one full request to load basic page (layout, css, javascript libraries) and then everything else is based on AJAX calls only. If ZK runs as a portlet, only the first request goes through portal infrastructure - subsequent AJAX calls runs as if the portal is not present, directly via ZK's servlet.

This has huge implications - every renderRequest/renderResponse specific informations (unfortunately large part of the portlet specification and tag libraries) are not available in AJAX calls. You can use a basic Liferay service (like UserLocalServiceUtil), but you can not access the current theme, user, portal context, ...

Hint: another problem is with using Servlet Filters (like OpenSessionInView) - you need to add <dispatcher>INCLUDE</dispatcher> to filter-mapping, otherwise the filter will not be used in the Portlet context.

To solve this problem, we customized the portlet to save basic Liferay context variables into ZK's session.

DLPortlet#

DLPortlet is based on ZK's DHtmlLayoutPortlet with small customizations.

Before the ZK page is processed with the original ZK's portlet method, DLPortlet calls setupSessionParameters(sess, request), which binds some usefull parameters into ZK's session:

  • WebKeys.THEME_DISPLAY - main Liferay ThemeDisplay object (request.getAttribute(WebKeys.THEME_DISPLAY)), it holds all bascic context parametrs - companyId, userId, portlet, ...
  • DLPortlet.ROLE_MAPPERS - map from portlet role to liferay role (set up in liferay-portlet.xml, role-mapper). We will need this map for custom security based on Liferay roles

One small additional enhancement - if you set DLPortlet.TITLE session attribute while processing the request (anywhere in ZK), DLPortlet will change the portlet title in Liferay accordingly.

DLLiferayService#

Based on session attributes bound via DLPortlet, you can use this service to access Liferay context informations even in AJAX calls. You can access companyId, userId, groupId, ... , security information with isUserInRole (see security chapter).

This service is best to use with some integration framework like Spring. This service is aware of portal mocking (see Mock Liferay portal chapter).

Access Liferay Services#

There is nothing special here. Liferay bounds Spring services into static utility classes with XxxLocalServiceUtil suffix (e.g. UserLocalServiceUtil) and they are available even in AJAX (or Servlet) context.

DLListboxLiferayController#

Check DLListbox page for basic information regarding our data driven listbox component. In normal circumstances you will use listbox based on database values with some ORM framework (like DLListboxCriteriaController for Hibernate Criteria API), but you can use DLListboxLiferayController controller to map directly to Liferay Dynamic API (which is based on Hibernate anyway).

The architecture is really similar to any other listbox controller:

 @ZkController
    DLLovboxController<User> lovboxOsobaResitelCtl = new DLLovboxGeneralController<User>(
           new DLListboxLiferayController<User>(User.class.getName() + "#lovboxOsobaResitelCtl")
    {

        @Override
        protected DLResponse<User> loadData(DynamicQuery dynamicQuery) throws SystemException {
            return new DLResponse<User>(
                    UserLocalServiceUtil.dynamicQuery(dynamicQuery),
                    (int)UserLocalServiceUtil.dynamicQueryCount(dynamicQuery)
            );
        }
    });
The example shows LOV (List Of Values) component, which contains listbox to select a Liferay user. As you can see, there is no code required, all filtering, sorting and paging is already acquired into dynamicQuery object. You can add some additional conditions and pass it into Liferay service.

Note that almost every Liferay service contains dynamicQuery and dynamicQueryCount methods (they are created by Liferay service builder), so you can access almost any Liferay entity this way.

JPA & Liferay#

Although Liferay Service Builder is quite a notable solution, we like the JPA standard more :-).

How to connect JPA entity with Liferay - just save only the id of Liferay entity and add object setter and getter, which access Liferay services.

 /** Liferay user id */
    private Long idLiferayUser;

   // omitted setters and getters for idLiferayUser

   /**
* Returns  Liferay user
*/
   public User getLiferayUser()
   {
        try {
            Long id = getIdLiferayUser();
            return id == null ? null : UserLocalServiceUtil.getUser(id);
        } catch (PortalException ex) {
            throw new LiferayException(ex);
        } catch (SystemException ex) {
            throw new LiferayException(ex);
        }
   }

   /**
* Sets Liferay user
*/
   public void setLiferayUser(User liferayUser)
   {
      setIdLiferayUser(liferayUser == null ? null : liferayUser.getPrimaryKey());
   }

This solution simulates @ManyToOne mapping - you can access user object with properties by object graph navigation. Usually you don't need to mind performance, because Liferay caches almost everything and basic entities are reasonably small tables anyway.

There is problem with a search/sort, you can do query by id (if you know it), or write custom SQL query (if you know, where the Liferay USER_ tables is located).

Liferay Security in a ZK application#

The Issue#

According to the portlet standard, you can use security by renderRequest.isUserInRole("ROLE_NAME"). But there is again the problem with portlet/servlet context - the renderRequest is available only in original request, but not in subsequent AJAX calls.

Luckily, you can use directly Liferay service:

 RoleLocalServiceUtil.hasUserRole(userId companyId, role, true);

There are two additional issues, which DLPortlet and DLLiferayService helps to solve:

  • where to get userId and companyId - it is held in ZK's session attribute WebKeys.THEME_DISPLAY.
  • how to translate portlet specific roles to Liferay roles - the mapping map is held in ZK's session attribute DLPortlet.ROLE_MAPPERS

Role mapping#

Role mapping is part of the portlet standard, however Liferay brings a custom solution in WEB-INF/liferay-portlet.xml:
 <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.0.0//EN"
        "[http://www.liferay.com/dtd/liferay-portlet-app_6_0_0.dtd">]
<liferay-portlet-app>

    <portlet>
        <portlet-name>HelpDesk</portlet-name>
    </portlet>

    <role-mapper>
        <role-name>administrator</role-name>
        <role-link>Administrator</role-link>
    </role-mapper>
    <role-mapper>
        <role-name>guest</role-name>
        <role-link>Guest</role-link>
    </role-mapper>
    <role-mapper>
        <role-name>power-user</role-name>
        <role-link>Power User</role-link>
    </role-mapper>
    <role-mapper>
        <role-name>user</role-name>
        <role-link>User</role-link>
    </role-mapper>
</liferay-portlet-app>

This means, that if you ask isUserInRole("power-user"), it is translated to call RoleLocalServiceUtil.hasUserRole(userId companyId, "Power User", true);.

Note that if the mapping is not found, Liferay will use the requested role name directly - this means that isUserInRole("Power User") will work as well, though not recommended.

Checking the user role#

DLLiferayService contains set of methods inspired by Spring Security:
  • isAnyGranted() - Returns true, if the user has at least one role from the list.
  • isAllGranted() - Returns true, if the user has all roles from the list.
  • isNoneGranted() - Returns true, if the user has no role from the list.
  • isUserInRole() - Check if the user is in role (user role mapper from liferay-portlet.xml).

All methods but isUserInRole() accept comma separated list of roles (e.g. isAnyGranted("administator,support,developer")).

Checking the user role from ZUL#

Expression Languge (EL) in ZUL can not evaluate methods with parameters, so we need to simulate this behaviour via Map interface. ZulRolesHelper class is only utility that translate "map calls" onto DLLiferayService methods explained in previous chapter.

The usage is than similar to DLLiferayService:

You need to make ZulRolesHelper object available with some ZK's variable resolver (we do this via spring bean).

Example Spring configuration:

 <!-- Init liferay services (XxxLocalServiceUtil) with default values -->
    <bean id="liferayMock" class="cz.datalite.zk.liferay.mock.LiferayMock" init-method="initLiferay"/>

    <!-- Main Liferay Access point for request bound objects (current company, group, user) -->
    <bean id="dlLiferayService" class="cz.datalite.zk.liferay.DLLiferayService">
        <property name="liferayMock" ref="liferayMock"/>
    </bean>

    <!-- Main Liferay Access point for request bound objects (current company, group, user) -->
    <bean id="dlLiferayRoles" class="cz.datalite.zk.liferay.security.ZulRolesHelper">
        <constructor-arg ref="dlLiferayService"/>
    </bean>

Mock Liferay portal#

Accessing Liferay services directly from ZK application is very convenient. However, development can be really slowed down with the long Liferay portal start up time. You can ease this a little with JRebel, but you will need to sometime restart the portal anyway. The idea here is to be able run the ZK portlet as a standard standalone ZK's application in a light-weight container (like Tomcat or Jetty) and start the portal only to debug the Liferay specific services.

The concept of mocking whole Liferay portal is similar to Unit tests and indeed we use Mockito framework to create basic Liferay services. There are several levels of integration. The mock (LiferayMock class) should check if the Liferay Portal is actually available and if not, mock all basic services (XxxLocalServiceUtil classes).

Mock all basic Liferay services (dummy implementation)#

The method will mock all basic services in PortalMockFactory.mockService():
 ....
        new LayoutSetLocalServiceUtil().setService(mock(LayoutSetLocalService.class, RETURNS_MOCKS));
        new LayoutSetPrototypeLocalServiceUtil().setService(mock(LayoutSetPrototypeLocalService.class, RETURNS_MOCKS));
        new LayoutTemplateLocalServiceUtil().setService(mock(LayoutTemplateLocalService.class, RETURNS_MOCKS));
        new LockLocalServiceUtil().setService(mock(LockLocalService.class, RETURNS_MOCKS));
        new MembershipRequestLocalServiceUtil().setService(mock(MembershipRequestLocalService.class, RETURNS_MOCKS));
        ....
Just swallow all calls and return stub implementation based on declared return type.

Sometimes, we need to be more specific what to return for some basic methods:

 new PhoneLocalServiceUtil().setService(mock(PhoneLocalService.class, RETURNS_MOCKS));
        when(PhoneLocalServiceUtil.getService().createPhone(anyLong())).thenReturn(new PhoneImpl());

        new CounterLocalServiceUtil().setService(mock(CounterLocalService.class, RETURNS_MOCKS));
        when(CounterLocalServiceUtil.getService().increment(anyString())).thenReturn(new Random().nextLong());

Check the implementation of required service to see, what happens.

PortalMockFactory.fillTestData#

This part is more interesting. We don't want to swallow every call or return empty list. For some services, we want to actually get some nice test data.

Just go to source codes to see what is available and what is not. Some example follows:

 when(PortletLocalServiceUtil.getPortletById(anyString())).thenReturn(new PortletImpl());

        // default and the only one company (portal instance)
        companyMockFactory.createCompanyImpl("company", CompanyMockFactory.DEFAULT_COMPANY_ID);

        // main group
        Group group = companyMockFactory.createGroupImpl("group", 1);

        // with serveral organizations
        Organization orgDefault = companyMockFactory.createOrganizationImpl("organization", 1);
        companyMockFactory.addOrganizationToGroup(orgDefault, group);

        Organization orgDatalite = companyMockFactory.createOrganizationImpl("datalite", 1);
        companyMockFactory.addOrganizationToGroup(orgDatalite, group);

        // and several users
        User admin = userMockFactory.createUserImpl("admin", 1);
        Contact adminContact = userMockFactory.createContactImpl("admin", 1);
        Address adminAddress = userMockFactory.createAddressImpl("admin", 1);
        userMockFactory.addContactToUser(adminContact, admin);
        userMockFactory.addAddressToContact(adminAddress, adminContact);

      ....

Return default user for some method in user service:

 when(UserLocalServiceUtil.getService().getUser(userId)).thenReturn(user);
        when(UserLocalServiceUtil.getService().getUserByEmailAddress(CompanyMockFactory.DEFAULT_COMPANY_ID,
                                     user.getEmailAddress())).thenReturn(user);
        when(UserLocalServiceUtil.getService().getUserById(userId)).thenReturn(user);
        when(UserLocalServiceUtil.getService().getUserById(CompanyMockFactory.DEFAULT_COMPANY_ID, userId)).thenReturn(user);
        when(UserLocalServiceUtil.getService().getUserByFacebookId(CompanyMockFactory.DEFAULT_COMPANY_ID, 
                                     user.getFacebookId())).thenReturn(user);

You can easily create your startup method and add any other mock data. Import Mockito and similar to these example, create test data and mock appropriate service method:

 import static org.mockito.Mockito.*;
0 Přílohy
11048 Zobrazení
Průměr (0 Hlasů)
Komentáře