Saturday, January 12, 2013

Liferay shared email address


The aim for this post is to show a possible solution for allowing multiple users with the same email address.
The solution is for Liferay 6.0.5, so compatibility with different versions might not apply.

The first task is to enable the registration with an email address that already exists.
In order to do this two things should be done: first, the constraint in the database must be deleted, then the check during the registration process must be eliminated.
The database constraint is a unique index on table _user consisting of two columns, the companyid and the emailaddress. In our test case, it has the name ix_615e9f7a, but this may vary. Its creation on Postgres database is the following:
CREATE UNIQUE INDEX ix_615e9f7a
  ON user_
  USING btree
  (companyid , emailaddress );
This index must be dropped, otherwise an SQL exception will be thrown during the addition or modification of a user if the email address already exists.

The next task is to eliminate the checks during the user save process that throw DuplicateUserEmailAddressException. This can be done by modifying the UserLocalServiceImpl class which is located in liferay-portal.war/WEB-INF/lib/portal-impl.jar/com/liferay/portal/service/impl.
This class has two protected void validate() functions with different arguments. In these function can be found the parts that should be eliminated:

protected void validate(long companyId, long userId,  boolean autoPassword, String password1, String password2,  boolean autoScreenName, String screenName, String emailAddress,  String firstName, String middleName, String lastName,  long[] organizationIds) throws PortalException, SystemException {     ....     if (Validator.isNotNull(emailAddress)) {       User user = this.userPersistence.fetchByC_EA(companyId, emailAddress);       if (user != null) {         throw new DuplicateUserEmailAddressException();       }     }     ..... }

protected void validate(long userId, String screenName,  String emailAddress, String firstName, String middleName,  String lastName, String smsSn) throws PortalException, SystemException {     .....       if ((Validator.isNotNull(emailAddress)) &&          (!user.getEmailAddress().equalsIgnoreCase(emailAddress)))       {         if (this.userPersistence.fetchByC_EA(           user.getCompanyId(), emailAddress) != null)         {           throw new DuplicateUserEmailAddressException();         }       }      ..... }
Now duplicated email addresses can be registered. But how can we guarantee that although the users share the same email address, they must not share their registration. In other words, authentication must be done without the email address.

The liferay-portal.war/WEB-INF/struts-config.xml should be edited. The following part is responsible for the login:

<action path="/login/login" type="com.liferay.portlet.login.action.LoginAction"> <forward name="portlet.login.login" path="portlet.login.login" /> </action> 
The simplest methood is to replace wit a custom Login action:
<action path="/login/login" type="com.liferay.portlet.login.action.MyLoginAction">
    <forward name="portlet.login.login" path="portlet.login.login" />
</action> 
Now the only thing to do, is to create a com.liferay.portlet.action.MyLoginAction. Here is a sample implementation. In this class, the original LoginAction is extended, and the some parts of the original methods are kept, but the login authentication is changed from email to screenname:

package com.liferay.portlet.login.action;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletPreferences;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.liferay.portal.kernel.util.ParamUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.model.Company;
import com.liferay.portal.model.CompanyConstants;
import com.liferay.portal.theme.ThemeDisplay;
import com.liferay.portal.util.PortalUtil;
import com.liferay.portal.util.PropsValues;
import com.liferay.portlet.login.util.LoginUtil;
public class MyLoginAction extends LoginAction {
    @Override
    protected void login(
            final ThemeDisplay themeDisplay, final ActionRequest actionRequest,
            final ActionResponse actionResponse, final PortletPreferences preferences)
            throws Exception {
        HttpServletRequest request = PortalUtil.getHttpServletRequest(actionRequest);
        HttpServletResponse response = PortalUtil.getHttpServletResponse(actionResponse);
        String login = ParamUtil.getString(actionRequest, "login");
        String password = ParamUtil.getString(actionRequest, "password");
        boolean rememberMe = ParamUtil.getBoolean(actionRequest, "rememberMe");
        String authType = CompanyConstants.AUTH_TYPE_SN;
        LoginUtil.login(request, response, login, password, rememberMe, authType);
        if (PropsValues.PORTAL_JAAS_ENABLE) {
            actionResponse.sendRedirect(themeDisplay.getPathMain() + "/portal/protected");
        }  else {
            String redirect = ParamUtil.getString(actionRequest, "redirect");
            if (Validator.isNotNull(redirect)) {
                redirect = PortalUtil.escapeRedirect(redirect);
                actionResponse.sendRedirect(redirect);
            }  else {
                actionResponse.sendRedirect(themeDisplay.getPathMain());
            }
        }
    }
} 



Wednesday, January 9, 2013

Migrating Liferay installation to different location

Imagine a scenario when you have a Liferay installation, but for various technical reasons, you have to move it to an other computer or just to a different folder. The other components of the system are not affected, the db connection, and all the connecting legacy system remain on the same location.

If the system setup is complicated enough, and the time frame is narrow, one of the simplest solutions is to just simply migrate the files to the different location.

Of course, some things should be done before this migration takes place. In this example, the Liferay version is 6.0.5 and is bundled with Glassfish 3.0.1.

  • The first thing to do is to delete content of the /domains/[domain-name]/generated-sources directory. This directory can be huge (several gigabytes), and it is a generated content used for caching, so there is no point in copying the data, it will be generated again, thus small performance decrease might occur during the first opening of a page.
  • The second important task is to delete the content of the /domains/[domain-name]/osgi-cache directory. If this directory is not emptied, then the Glassfish domain cannot be started, because the osgi-cache holds references for the absolute paths of some of the osgi jars.

After this two deletions are made, the Liferay installation can be moved to the new direction and if no other configuration is required (depends on the environment), it can be started safely.

Saturday, January 5, 2013

Count records based on a criteria for each day in postgres

Suppose we have a table in the database, where the last login times of the users are stored, and we have some data in it, like the following table:

userid last_login
651651 2013-01-05 21:39:45
894156 2013-01-04 11:31:00
132546 2013-01-04 15:01:49
654713 2013-01-04 01:05:16
654231 2013-01-02 16:40:57
687413 2012-12-31 23:40:57

We would like to know on each day, how many users logged in last, so wee need an SQL to list all days and select the count of users last logged in on that day.

The first task is to find an SQL with which consecutive days can be listed.
The query we are looking for is the following:

select i::date from 
 generate_series('2012-12-30', '2013-01-05', '1 day'::interval) i;
This produces the output:

day
2012-12-30
2012-12-31
2013-01-01
2013-01-02
2013-01-03
2013-01-04
2013-01-05

Great, now we need a criteria for the last_login for each day. The previous query generates timestamps with 00:00:00.000, so we need an interval with the current day as the beginning, and the next day as the end of the interval we are looking for.
Withe the help of the query

select (i::date + '1 day'::interval) from 
 generate_series('2012-12-30', '2013-01-05', '1 day'::interval) i;
we will be able to accomplish this task. Its output is quite similar with the previous one, but in this case the result set is shifted into the future with one day (and the precision changed, but this is not important for our point of view, because we could achieved the same precision with our first query selecting i:timestamp instead of i:date).

day
2012-12-31 00:00:00.000
2013-01-01 00:00:00.000
2013-01-02 00:00:00.000
2013-01-03 00:00:00.000
2013-01-04 00:00:00.000
2013-01-05 00:00:00.000
2013-01-06 00:00:00.000

Superb, we now only need to combine the queries to build our desired selection:

select to_char(i::date, 'YYYY.MM.DD') as last_login_day, 
count(u.userid) as users
from generate_series('2012-12-30', '2013-01-05', '1 day'::interval) i,
 users u
where (i::date + '1 day'::interval) > u.last_login 
 and i::date < u.last_login 
group by last_login_day order by last_login_day:

The result shows the correct values, but unfortunately, the days which none of the users chose to be their last login date are not show up in the result set:

last_login_day users
2012-12-31 1
2013-01-02 1
2013-01-04 3
2013-01-05 1

This is actually not a big problem, and can be solved by using a union. We create an other selection for displaying all days with 0 user counts, and take the union of the two results by grouping by the days and summing up the users (of course the additional order by is needed as well):

select tmp.last_login_day, sum(tmp.users) from (
 (
 select to_char(i::date, 'YYYY.MM.DD') as last_login_day, 
   count(u.userid) as users
 from generate_series('2012-12-30', '2013-01-05', '1 day'::interval)
   i, users u
 where (i::date + '1 day'::interval) > u.last_login 
   and i::date < u.last_login
 group by last_login_day order by last_login_day
 ) union (
 select to_char(i::date, 'YYYY.MM.DD') as last_login_day,
   0 as users
 from generate_series('2012-12-30', '2013-01-05', '1 day'::interval) i
 )
) as tmp group by 1 order by 1

The results are what we wanted to achieve in the first place:

last_login_day users
2012-12-30 0
2012-12-31 1
2013-01-01 0
2013-01-02 1
2013-01-03 0
2013-01-04 3
2013-01-05 1