Pages

Monday, October 15, 2012

Facebook similar chat for your Java web application.

Chatting is easy just like eating a piece of cake or drinking a hot coffee. Have you ever thought about developing a chat program by yourself?. You know that, it is not easy as chatting. But, if you are a developer and if you read to the end of this article, you may put a try to develop a chatting application by your self and allow your users to chat via your web application. 

I had to implement a chatting application for my web application. As every one does, I started to search on internet. I found IRC. When I read and search more about IRC, I understood that finding a web base client for IRC was difficult. I wanted to have more customizable web client which is working similar to Facebook. At last and luckily, I found CometD. 

Finally, I was able to implement chatting application by using CometD and more customizable chat windows opening on the browser which is exactly similar to Facebook. This works almost all the modern browsers. This article explains step by step, How to implement chatting application from the scratch and also How to integrate chatting application to your existing Java base web application. Remember, Your web application should be a Java base one.

You need to download the cometD from their official web site. It has all the dependencies required to implement the chatting application except tow java script libraries. I have written two Javascript libraries, one to create dynamic chat windows like Facebook and other to handle CometD chatting functionality in generic way. If you can manage these stuff by your self, you don't need to use those tow Javascript libraries. Actually, CometD documentation provides good details. But, I go ahead with the tutorial by using those tow libraries. Any way, I recommend first use those tow libraries and then customize it as you need. I hope to share the sample application with you and you can deploy it in your localhost and test, how it works.

1.Adding required jar files. 

If you use maven to build your project, add the following dependencies into your pom.xml file

<dependencies>
    <dependency>
        <groupId>org.cometd.java</groupId>
        <artifactId>bayeux-api</artifactId>
        <version>2.5.0</version>
    </dependency>
    <dependency>
         <groupId>org.cometd.java</groupId>
         <artifactId>cometd-java-server</artifactId>
         <version>2.5.0</version>
    </dependency>
    <dependency>
         <groupId>org.cometd.java</groupId>
         <artifactId>cometd-websocket-jetty</artifactId>
         <version>2.5.0</version>
         <exclusions>
             <exclusion>
                 <groupId>org.cometd.java</groupId>
                 <artifactId>cometd-java-client</artifactId>
             </exclusion>
         </exclusions>
   </dependency>
   <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.6.6</version>
   </dependency>
   <dependency>
        <groupId>org.cometd.java</groupId>
        <artifactId>cometd-java-annotations</artifactId>
        <version>2.5.0</version>
   </dependency>
</dependencies>

If you are not using maven to build your project, just copy the following .jar files into /WEB-INF/lib folder from your CometD download bundle. You can find these .jar files from /cometd-demo/target/cometd-demo-2.5.0.war file.

  • bayeux-api-2.5.0.jar
  • cometd-java-annotations-2.5.0.jar
  • cometd-java-common-2.5.0.jar
  • cometd-java-server-2.5.0.jar
  • cometd-websocket-jetty-2.5.0.jar
  • javax.inject-1.jar
  • jetty-continuation-7.6.7.v20120910.jar
  • jetty-http-7.6.7.v20120910.jar
  • jetty-io-7.6.7.v20120910.jar
  • jetty-jmx-7.6.7.v20120910.jar
  • jetty-util-7.6.7.v20120910.jar
  • jetty-websocket-7.6.7.v20120910.jar
  • jsr250-api-1.0.jar
  • slf4j-api-1.6.6.jar
  • slf4j-simple-1.6.6.jar

2.Adding required Javascript files.

You need to link the following Javascript files.

  • cometd.js
  • AckExtension.js
  • ReloadExtension.js
  • jquery-1.8.2.js
  • jquery.cookie.js
  • jquery.cometd.js
  • jquery.cometd-reload.js
  • chat.window.js
  • comet.chat.js
The 'chat.window.js' and 'comet.chat.js' are my own tow Javascript libraries which does not come with CometD distribution. If you are totally following this tutorial, you have to link those tow libraries as well. Provided sample application has these tow Javascript libraries. 

3.Writing chat service class.

/**
 * @author Semika siriwardana
 * CometD chat service.
 */
package com.semika.cometd;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.inject.Inject;

import org.cometd.annotation.Configure;
import org.cometd.annotation.Listener;
import org.cometd.annotation.Service;
import org.cometd.annotation.Session;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.cometd.server.filter.DataFilter;
import org.cometd.server.filter.DataFilterMessageListener;
import org.cometd.server.filter.JSONDataFilter;
import org.cometd.server.filter.NoMarkupFilter;

@Service("chat")
public class ChatService {
    private final ConcurrentMap<String, Map<String, String>> _members = new ConcurrentHashMap<String, Map<String, String>>();
    @Inject
    private BayeuxServer _bayeux;
    @Session
    private ServerSession _session;

    @Configure ({"/chat/**","/members/**"})
    protected void configureChatStarStar(ConfigurableServerChannel channel) {
        DataFilterMessageListener noMarkup = new DataFilterMessageListener(new NoMarkupFilter(),new BadWordFilter());
        channel.addListener(noMarkup);
        channel.addAuthorizer(GrantAuthorizer.GRANT_ALL);
    }

    @Configure ("/service/members")
    protected void configureMembers(ConfigurableServerChannel channel) {
        channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
        channel.setPersistent(true);
    }

    @Listener("/service/members")
    public void handleMembership(ServerSession client, ServerMessage message) {
        Map<String, Object> data = message.getDataAsMap();
        final String room = ((String)data.get("room")).substring("/chat/".length());
        
        Map<String, String> roomMembers = _members.get(room);
        if (roomMembers == null) {
            Map<String, String> new_room = new ConcurrentHashMap<String, String>();
            roomMembers = _members.putIfAbsent(room, new_room);
            if (roomMembers == null) roomMembers = new_room;
        }
        final Map<String, String> members = roomMembers;
        String userName = (String)data.get("user");
        members.put(userName, client.getId());
        client.addListener(new ServerSession.RemoveListener() {
            public void removed(ServerSession session, boolean timeout) {
                members.values().remove(session.getId());
                broadcastMembers(room, members.keySet());
            }
        });

        broadcastMembers(room, members.keySet());
    }

    private void broadcastMembers(String room, Set<String> members) {
        // Broadcast the new members list
        ClientSessionChannel channel = _session.getLocalSession().getChannel("/members/"+room);
        channel.publish(members);
    }

    @Configure ("/service/privatechat")
    protected void configurePrivateChat(ConfigurableServerChannel channel) {
        DataFilterMessageListener noMarkup = new DataFilterMessageListener(new NoMarkupFilter(),new BadWordFilter());
        channel.setPersistent(true);
        channel.addListener(noMarkup);
        channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
    }

    @Listener("/service/privatechat")
    protected void privateChat(ServerSession client, ServerMessage message) {
        Map<String,Object> data = message.getDataAsMap();
        
        String room = ((String)data.get("room")).substring("/chat/".length());
        Map<String, String> membersMap = _members.get(room);
        if (membersMap == null) {
            Map<String,String>new_room=new ConcurrentHashMap<String, String>();
            membersMap=_members.putIfAbsent(room,new_room);
            if (membersMap==null)
                membersMap=new_room;
        }
        
        String peerName = (String)data.get("peer");
        String peerId = membersMap.get(peerName);
        
        if (peerId != null) {
            
         ServerSession peer = _bayeux.getSession(peerId);
            
            if (peer != null) {
             Map<String, Object> chat = new HashMap<String, Object>();
                String text = (String)data.get("chat");
                chat.put("chat", text);
                chat.put("user", data.get("user"));
                chat.put("scope", "private");
                chat.put("peer", peerName);
                ServerMessage.Mutable forward = _bayeux.newMessage();
                forward.setChannel("/chat/" + room);
                forward.setId(message.getId());
                forward.setData(chat);

                if (text.lastIndexOf("lazy") > 0) {
                    forward.setLazy(true);
                }
                if (peer != client) {
                    peer.deliver(_session, forward);
                }
                client.deliver(_session, forward);
            }
        }
    }

    class BadWordFilter extends JSONDataFilter {
        @Override
        protected Object filterString(String string) {
            if (string.indexOf("dang") >= 0) {
                throw new DataFilter.Abort();
            }
            return string;
        }
    }
}

4.Changing web.xml file.

You should add the following filter into your web.xml file.

<filter>
     <filter-name>continuation</filter-name>
     <filter-class>org.eclipse.jetty.continuation.ContinuationFilter</filter-class>
</filter>
<filter-mapping>
     <filter-name>continuation</filter-name>
     <url-pattern>/cometd/*</url-pattern>
</filter-mapping>

And also the following servlet.

<servlet>
    <servlet-name>cometd</servlet-name>
    <servlet-class>org.cometd.annotation.AnnotationCometdServlet</servlet-class>
    <init-param>
         <param-name>timeout</param-name>
         <param-value>20000</param-value>
    </init-param>
    <init-param>
         <param-name>interval</param-name>
         <param-value>0</param-value>
    </init-param>
    <init-param>
         <param-name>maxInterval</param-name>
         <param-value>10000</param-value>
    </init-param>
    <init-param>
         <param-name>maxLazyTimeout</param-name>
         <param-value>5000</param-value>
    </init-param>
    <init-param>
         <param-name>long-polling.multiSessionInterval</param-name>
         <param-value>2000</param-value>
    </init-param>
    <init-param>
         <param-name>logLevel</param-name>
         <param-value>0</param-value>
    </init-param>
    <init-param>
         <param-name>transports</param-name>
         <param-value>org.cometd.websocket.server.WebSocketTransport</param-value>
    </init-param>
    <init-param>
         <param-name>services</param-name>
         <param-value>com.semika.cometd.ChatService</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>cometd</servlet-name>
    <url-pattern>/cometd/*</url-pattern>
</servlet-mapping>

5.Implementing client side functions.

I think this section should be descriptive. If you allows your users to chat with other users, you need to show the list of online users in you web page, just like Facebook shows the online users inside the right side bar. For that, you can place a simple <span> or <div> tag inside your page. I have done it as follows.
<div id="members"></div>

All the online users will be displayed with in the above container. Once you click on a particular user name, it will open a new chat window similar to Facebook. For each pair of users, it will open a new chat window. To get this behaviour, you should use 'chat.window.js' which I mentioned before. Chatting in between particular pair of users will continue through a dedicated chat window. 

Just after user is logging into your web application as usual way, we should subscribe that user to chat channels. You can do it using the following way. 

 
$(document).ready(function(){ 
    $.cometChat.onLoad({memberListContainerID:'members'});
});

Note that, I have passed the 'id' of online user list container as a configuration parameter. Then, user should be joined with channel as follows.You can call the bellow method with the username.
function join(userName){
   $.cometChat.join(userName);
}

Since for each chat, there is a dedicated chat window just like Facebook, we should maintain global Javascript array to store those created chat window objects. You need to place the following Javascript code inside your page.
function getChatWindowByUserPair(loginUserName, peerUserName) {
    var chatWindow;  
    for(var i = 0; i < chatWindowArray.length; i++) {
       var windowInfo = chatWindowArray[i];
       if (windowInfo.loginUserName == loginUserName && windowInfo.peerUserName == peerUserName) {
           chatWindow =  windowInfo.windowObj;
       }
    }
    return chatWindow;
}
 
function createWindow(loginUserName, peerUserName) {  
    var chatWindow = getChatWindowByUserPair(loginUserName, peerUserName);
    if (chatWindow == null) { //Not chat window created before for this user pair.
       chatWindow = new ChatWindow(); //Create new chat window.
       chatWindow.initWindow({
             loginUserName:loginUserName, 
             peerUserName:peerUserName,
             windowArray:chatWindowArray});
   
       //collect all chat windows opended so far.
       var chatWindowInfo = { peerUserName:peerUserName, 
                              loginUserName:loginUserName,
                              windowObj:chatWindow 
                            };
   
       chatWindowArray.push(chatWindowInfo);
   }
   chatWindow.show();  
   return chatWindow;
}

As I mentioned above, declare following global Javascript variable.
var chatWindowArray = [];   
var config = {
    contextPath: '${pageContext.request.contextPath}'
};

Since I am using a JSP page, I have to get the context path via 'pageContext' variable. If you are using a HTML page, manage it by your self to declare 'config' Javascript global variable.
Now, you almost reached to last part of the tutorial.

5.How does the sample application works?

You can download the comet.war file and deploy it in your server. Point the browser to following URL.

http://localhost:8080/comet

This will bring you to a page which has a text field and button called "Join". Insert some user name as you wish and click on "Join" button. Then you will be forwarded to a another page which has list of online users. Your name is highlighted in red color. To chat in your local machine, You can open another browser (IE and FF) and join to the chat channel. The peer user displays in blue color in the online users list. Once you click on a peer user, it will open a new chat window so that You can chat with him. This functions very similar to Facebook chatting. 

I have tested this chatting application in IE, FF and Crome and works fine. If you want any help of integrating this with your Java base web application, just send me a mail.


You may also like:

Friday, August 24, 2012

Passing more parameters with login request - Java

If you are using some container managed authentication mechanism like standard java form based authentication or spring security authentication mechanism, You are not really involving with user credential validation, like checking user name and password against the database, but the container is fully responsible for this. But what will happen if you want to pass some additional parameters with log-in details(username and password)?. The well known and most common scenario is passing "Keep me logged in" or "Remember me" check box value with your log-in details and doing some work with that while container is authenticating the user. 

Recently, I had to implement the "Keep me logged in" function for one of my current project which are using spring 3 security as authentication mechanism. I am little new to spring 3 and it was challenging work for me of passing "Keep me logged in" check box status into spring's authentication provider class. 

With this post, I will explain, How I achieved that. The application uses 'AuthenticationProvider' class which extends from spring's 'AbstractUserDetailsAuthenticationProvider' and overrides 'retrieveUser' method which returns spring's UserDetails instance. Normally, authentication details are provided to 'retrieveUser' method via spring's 'WebAuthenticationDetails' instance.
Bellow shows the snip of code from 'retrieveUser' method of my authentication provider class.

@Override
protected User retrieveUser(String userName, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {

     OGGER.debug("Retrieve user : " + userName);
        
     final String password = authentication.getCredentials().toString();

     WebAuthenticationDetails webAuthenticationDetails = authentication.getDetails());

     try {
         User user = userBusiness.getUserByUsernameAndPassword(userName, password);
         logger.debug("Remote address : " + webAuthenticationDetails.getRemoteAddress());
         logger.debug("Session Id     : " + webAuthenticationDetails.getSessionId());
         //.................
            
         return user;
     } catch (Exception e) {
         e.printStackTrace();
     }
}

I wanted to get "Keep me logged in" check box value into 'retrieveUser' method. It was very obvious that I am not able to get the check box value with current situation. The 'WebAuthenticationDetails' provides some details like remote address, session id etc, But not our own additional details.

As the next step, I implemented my own custom class by extending 'WebAuthenticationDetails' and put 'rememberMe' as a bean property.That class shows bellow.

package com.blimp.webapp.security;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.web.authentication.WebAuthenticationDetails;

/**
 * @author semika
 *
 */
public class BlimpAuthenticationDetails extends WebAuthenticationDetails {

    private static final long serialVersionUID = 2012033417540858020L;
 
    private String rememberMe;
 
    public String getRememberMe() {
     return rememberMe;
    }
 
    //This constructor will be invoked by the filter
    public BlimpAuthenticationDetails(HttpServletRequest request) {
     super(request);
     this.rememberMe = request.getParameter("rememberMe");
    }
}

The next thing is, How we tell spring security engine to use my custom authentication detail class instead of using 'WebAuthenticationDetails' class when authenticating a user?.
For this one, we have to configure authentication processing filter in our spring security xml file. Some filtered contents from security XML file are shown bellow.

<http auto-config="false">
     <custom-filter ref="authenticationProcessingFilter" before="FORM_LOGIN_FILTER"/>
     <form-login login-page="/login.jsp?type=login"  authentication-failure-url="/login.jsp?login_error=true" default-target-url="/getRedirectPage.htm"/>
     <intercept-url pattern="/**" access="ROLE_ADMIN,ROLE_USER"  />
</http>
<authentication-manager alias="authenticationManager">
     <authentication-provider ref="daoAuthenticationProvider" />
</authentication-manager>
<beans:bean id="authenticationProcessingFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
     <beans:property name="authenticationManager" ref="authenticationManager"/>
     <beans:property name="authenticationDetailsSource">
          <beans:bean class="org.springframework.security.authentication.AuthenticationDetailsSourceImpl">
  <beans:property name="clazz" value="com.blimp.webapp.security.BlimpAuthenticationDetails"/>
   </beans:bean>
     </beans:property>
</beans:bean>

The 'daoAuthenticationProvider' is the instance of my 'AuthenticationProvider' class which extends 'AbstractUserDetailsAuthenticationProvider' and it has overridden 'retrieveUser' method. The my updated 'retrieveUser' method will be as follows.

@Override
protected User retrieveUser(String userName, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {

     OGGER.debug("Retrieve user : " + userName);
        
     final String password = authentication.getCredentials().toString();

     BlimpAuthenticationDetails webAuthenticationDetails = ((BlimpAuthenticationDetails) authentication.getDetails());

     try {
         User user = userBusiness.getUserByUsernameAndPassword(userName, password);
         logger.debug("Remote address : " + webAuthenticationDetails.getRemoteAddress());
         logger.debug("Session Id     : " + webAuthenticationDetails.getSessionId());
         logger.debug("Remember me    : " + webAuthenticationDetails.getRememberMe());
         //.................
            
         return user;
     } catch (Exception e) {
         e.printStackTrace();
     }
}

As you can see above, I can get the remember me check box value from 'retrieveUser' method.
You may also like:

Sunday, August 5, 2012

Java class for Valums AJAX file uploader for IE9 with spring MVC

I wanted to have AJAX file upload which supports multiple files uploading for my recent project. The Valums AJAX file up-loader gave me a very good experience and integrated that into my application. This file up-loader uses XHR for uploading files on the browsers which supports XMLHttpRequest level 2 and falls back to hidden iframe based upload in other browsers. 

My application should run in FF, Chrome and IE. IE9 does not support XHR file uploads and even Valums GitHub has only provided a java example for XHR Level 2 supporting browsers which directly reads the input stream from the request. 

For browsers which do not support XHR Level 2, up-loader send the file to server as 'multipart/form-data'. Therefor, we should write our up-loader java controller class so that it reads multipart form data when request coming from browsers which do not support XHR file upload (like IE9) and directly read the input stream for the requests coming from the browsers which support XHR file uploads (like FF3.6+, Safari4+, Chrome). 

In both type of browsers, we should make sure that the response type as 'text/plain'. My application is running with spring 3 MVC which persuaded me to do some work around when reading multipart form data from the request. In spring 3 MVC environment, 'HttpServletRequest' which has 'multipart/form-data' are wrapped into 'MultipartHttpServletRequest', not like in normal servlet environment. 

Here, I am going to give you an example and basic java code which reads multipart form data with spring 3 MVC environment. If you are not using, spring 3 MVC and using some other controllers like pure servlets or struts 2 action class, you can directly read the multipart form data by using HttpServletRequest. 

However, for Valums file up-loader, we should write our controller class in tow conditional way. One is for XHR file upload supporting browsers and one for 'multipart/form-data' based file uploading browsers.

@RequestMapping(value = "*ajax*", method = RequestMethod.POST)
public @ResponseBody
String uploadFile(HttpServletRequest request, HttpSession session, HttpServletResponse response, Principal principal, Model model, Locale locale) throws IOException, ServletException {

   InputStream is = null;
   String filename = null; 
   String result = null; 
   try {
      
       if (isMultipartContent(request)) { 
            MultipartHttpServletRequest mrequest = (MultipartHttpServletRequest)request;
            Map<String MultipartFile> fileMap = mrequest.getFileMap();           
            for (Map.Entry<String MultipartFile> entry : fileMap.entrySet()) {
               MultipartFile mfile = entry.getValue(); 
               is = mfile.getInputStream();
               filename = mfile.getOriginalFilename();               
               break;
            }
       } else {
           filename = request.getHeader("X-File-Name");
           is = request.getInputStream();
       }
      
       result = "{success:true}";

   } catch (Exception ex) {
       ex.printStackTrace();
       result = "{success:false}";  
   } finally {
       try {
    is.close();
       } catch (IOException ignored) {}
   } 
   return result;
}


private static final boolean isMultipartContent(HttpServletRequest request) {
    String contentType = request.getContentType();
    if (contentType == null) {
     return false;
    }
    if (contentType.toLowerCase().startsWith("multipart/")) {
     return true;
    }
    return false;
}


I hope this will help you. At the beginning, even I thought, Valums ajax file up-loader does not work in IE9. But it works fine in IE as well. If you need any help, feel free to put comment or send a mail to me.
You may also like:

Thursday, July 19, 2012

Spring 3 Internationalization and Localization - Not 'Hello World', But 'Practical'

I wanted to add internationalization and localization feature provided by spring 3 to one of my current project recently. I went through the spring documentation and then searched on internet to find some resources. 
But I could not find a resource which was able to satisfy my client requirement. Most of the tutorials are like hello world application which gives basic understanding. Even spring documentation does not give in detailed explanation on integrating this feature to our own project. Expert developers can pick the stuff from spring documentation. But for others, have to put extra effort to make things up and running. With this tutorial, I am going to explain very practical scenario that most of the clients are expecting.

The requirement

I am using spring security with my application. User should be able to select the language from the log-in page which was specified as 'login-page' of spring security XML file. I have provided links as "English","Chinese","German" and "Spanish" on top right corner of my log-in page to select the language. User can select the language and log in to the system by providing username and password. Then the whole application should be from the selected language. And also when selecting the language from the log-in page, the contents of the log-in page should also be changed.

Spring configurations

As the first step, I had to configure LocaleChangeInterceptor interceptor with in the dispatcher-servlet.xml file. This XML file name will change according to the name given to DispatcherServlet in web.xml file. I have given 'dispatcher' as the name for DispatcherServlet. So I should create 'dispatcher-servlet.xml' file under /WEB-INF folder. My application is running on Tomcat 7.

I could not make it working by following the way of declaring this interceptor as in the spring documentation. The request for changing the locale before log in(ie: from the login page) was not intercepted by the locale change interceptor. Therefore, I had to declare it as fallows.

<mvc:interceptors>
  <mvc:interceptor>
     <mvc:mapping path="/doChangeLocale*"/>
         <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" >
             <property name="paramName" value="locale" />
         </bean>
    </mvc:interceptor>
</mvc:interceptors>

The 'LocaleChangeInterceptor' will intercept the request asking for locale change and the corresponding locale code will be stored in the session with the help of 'SessionLocaleResolver'.

Next we will look at how to declare the 'SessionLocaleResolver' in the 'dispatcher-servlet.xml' file.

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
     <property name="defaultLocale" value="en" />
</bean>

The SessionLocaleResolver will store the locale in the current session and resolve it for every subsequent user requests for the current session.

Next, we have to declare the message resource bean.

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="useCodeAsDefaultMessage" value="true" />
    <property name="basenames">
         <list>
            <value>classpath:messages</value>
         </list>
    </property>
    <property name="cacheSeconds" value="0" />
    <property name="defaultEncoding" value="UTF-8"></property>
</bean>

My application should support for 4 languages. So I added 4 property files into the 'resources' folder (ultimately all those property files should be in 'classes' folder) as follows.

messages_de.properties - German
messages_en.properties - English
messages_zh.properties - Chinese
messages_es.properties - Spanish

Note that, all the file names should start with the text which you specified as 'basenames' property of message resource bean.

The spring 3 security configurations were very important in this implementation. Keep in mind that, when you click any locale change link from the log-in page, you are not authenticated yet. But still that request should be intercepted by 'LocaleChangeInterceptor'. Otherwise, the language will not be changed as expected. There fore, any anonymous user should be allowed to make locale change request and that request should go through the 'LocaleChangeInterceptor'. 

Carefully look into my spring security configuration.
<http auto-config="false">
    <form-login login-page="/login.jsp"  authentication-failure-url="/login.jsp?login_error=true" default-target-url="/mainMenu.htm"/>
    <logout logout-success-url="/login.jsp"/>
    <intercept-url pattern="/doChangeLocale**" access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/>
    <intercept-url pattern="/**" access="ROLE_ADMIN,ROLE_USER"  />
</http>

The login.jsp file is where user can log into the system by providing username and password and also that page has the corresponding links to change the locale. When user makes any request to a protected resource without authenticating, the user will be redirected to the login.jsp page. The above configuration says all the requests that are coming to the application should be from a authenticated user and also the user should be authorized except for the '/doChangeLocale**' request.

The intercept URL '/doChangeLocale**' is very important. Without that, the requests for changing locales are not intercepted by the locale change interceptor and finally locale will not change.

The followings are the locale change links that are placed in the login.jsp file.

<a href="<%=request.getContextPath()%>/doChangeLocale?locale=en">English</a>
<a href="<%=request.getContextPath()%>/doChangeLocale?locale=de">German</a>
<a href="<%=request.getContextPath()%>/doChangeLocale?locale=es">Spanish</a>
<a href="<%=request.getContextPath()%>/doChangeLocale?locale=zh">Chinese</a>


Hope this will be helpful for you.
You may also like:

Saturday, June 2, 2012

How to configure Apache HTTP server with Tomcat on SSL ?

Introduction

I recently needed to simulate our production deployment environment in my local machine. Our production applications are running on Apache HTTP server. We deploy application on multiple tomcat instances and also different Tomcats for different URL name spaces. Apache HTTP server is responsible as front door for all these. Apache HTTP server connects to the tomcat with mod_jk over mod_ssl. When Apache HTTP server receives a request, it checks for the requests and forward to the tomcat accordingly. This configuration is important for security and clustering.

This tutorial contains following section.
  1. Installing and configuring Apache HTTP server. 
  2. Installing and configuring Apache tomcat. 
  3. Installing and configuring mod_jk. 
  4. Configuring mod_ssl. 
  5. Testing the environment.

Installing and configuring Apache HTTP server.

Run the following command to download Apache HTTP server 2.2.22

wget http://mirrors.gigenet.com/apache//httpd/httpd-2.2.22.tar.bz2
wget http://www.apache.org/dist/httpd/httpd-2.2.22.tar.bz2.md5
md5sum -c httpd-2.2.22.tar.bz2.md5


After executing above command, httpd-2.2.22.tar.bz2 archive will be downloaded to your "Downloads" folder. Also you can directly download the apache server from the following location as well.

http://httpd.apache.org/download.cgi#apache22

To extract the zipped archive file, run the following command from /Downloads folder.

cd /home/semika/Downloads
tar -xjvf httpd-2.2.22.tar.bz2


Above command will extract the zipped archive file into httpd-2.2.22 folder under Downloads folder. Now, you should decide where you are going to install Apache HTTP server. I am going to install it to /home/semika/httpd-2.2.22 folder. You have to create the folder there. Navigate to your user folder and execute the following command to create new folder.

cd /home/semika
mkdir httpd-2.2.22

To install Apache to your particular platform, we need to compile the source distribution that we have already downloaded. If you see carefully inside the extracted folder under Downloads/httpd-2.2.22, you can see there is a configure script. We can compile the source distribution with that script and it will create necessary stuff to install Apache HTTP server.

When compiling Apache, various options can be specified that are suited to our local environment. For the complete reference of options provided, see here.

Since, we need mod_ssl to be configured with Apache compilation, we need to install OpenSSL development bundle. Otherwise, compilation will raise an exception. To install OpenSSL development libraries, run the following command.

sudo apt-get install openssl libssl-dev

Sometime, You might need to run the following command as well, if you encounter an error while apache compilation.

sudo apt-get install zlib1g-dev
sudo apt-get install libxml2-dev

To compile Apache source distribution, execute the following commands.

cd /home/semika/Downloads/httpd-2.2.22

./configure --prefix=/home/semika/httpd-2.2.22 --enable-mods-shared=all --enable-log_config=static --enable-access=static --enable-mime=static --enable-setenvif=static --enable-dir=static -enable-ssl=yes


--prefix                        : You can specify the installation directory
--enable-mods-shared : Setting this to 'all' will enable to install all the shared modules.
--enable-ssl                 : Since we are going to configure Apache HTTP server with mod_ssl, this has been set to 'yes' to compile Apache with mod_ssl. By default this option is disabled.

For other options specified in the configuration command, please look into full options reference documentation. After successfully running the above command, execute the following commands. Before execute the following command, just have look on your specified installation directory, ie:/home/semika/httpd-2.2.22, you can see that it is still empty.

make
make install

Now, you can see Apache HTTP server has been installed under /home/semika/httpd-2.2.22. Look for modules folder, you can see the list of modules installed. Confirm, whether it has installed mod_ssl.so. Now, you can start the Apache HTTP server.

cd /home/semika/httpd-2.2.22/bin
sudo ./apachectl start

If you see bellow line when executing the above command, 

"httpd: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1 for ServerName

edit your /httpd-2.2.22/conf/httpd.conf file as follows. Look for "ServerName" property in httpd.conf file. You will see following line there.

#ServerName www.example.com:80

Uncomment this line and modify it as follows.

ServerName localhost

Again start the Apache HTTP server. If you did not change the default port which Apache server is running, check the following URL to check whether server is started or not. The default Apache HTTP server running port is 80. 

http://localhost/

With the above URL, if you see a page with "It works", you are done with Apache HTTP server.

Further, if you want to change the default port, you can edit the httpd.conf file as follows. Look for "Listen" property and change it as you wish.

I have set it to 7000. So my Apache HTTP server is running on 7000. I have to access the following URL to get "It works" page.

http://localhost:7000/

To stop Apache HTTP server,execute the following command.
cd /home/semika/httpd-2.2.22/bin
sudo ./apachectl stop

Installing and configuring Apache tomcat. 

Installing and configuring Apache tomcat is not a big thing, if you are involving with this kind of advance configuration. For the completeness of the tutorial, I will explain that a little. I am using Apache Tomcat 7.0.25. You can download it from Apache web site and extract it to some where in you local machine. After that, you have to set the environment variable as follows.

export CATALINA_HOME=/home/semika/apache-tomcat-7.0.25
export PATH=$CATALINA_HOME/bin:$PATH

You can start the tomcat with following command.

cd home/semika/apache-tomcat-7.0.25/bin
./startup.sh

If you want to see tomcat's console out put, execute the following commands before starting the tomcat.

cd home/semika/apache-tomcat-7.0.25/logs/
tail -f catalina.out 

After successfully starting the tomcat, try the following URL

http://localhost:8080/ 

By default, tomcat will run on port 8080. Now our Apache HTTP server is running on port 7000 and tomcat is on 8080. 

Further, to configure Tomcat with Apache HTTP server, we need to create workers.properties file under home/semika/apache-tomcat-7.0.25/conf/. 

workers.properties
# Define 1 real worker named ajp13
worker.list=ajp13

# Set properties for worker named ajp13 to use ajp13 protocol,
# and run on port 8009
worker.ajp13.type=ajp13
worker.ajp13.host=localhost
worker.ajp13.port=8009
worker.ajp13.lbfactor=50
worker.ajp13.cachesize=10
worker.ajp13.cache_timeout=600
worker.ajp13.socket_keepalive=1
worker.ajp13.socket_timeout=300

Apache HTTP server will connect to Tomcat through port 8009. If you see server.xml file under Tomcat's conf folder, you can see following connector declaration there.

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

Installing and configuring mod_jk

If you look into httpd-2.2.22/modules folder, you can see mod_jk.so has been installed. The mod_jk is a connector for Apache HTTP server to connect to Apache Tomcat. To configure, you have to load mod_jk.so module to Apache HTTP server.

Edit /httpd-2.2.22/conf/httpd.conf file as follows. You need to add these properties.

# Load mod_jk module
# Update this path to match your modules location
LoadModule jk_module modules/mod_jk.so

# Where to find workers.properties
# Update this path to match your conf directory location
JkWorkersFile /home/semika/apache-tomcat-7.0.25/conf/workers.properties

# Where to put jk logs
# Update this path to match your logs directory location
JkLogFile /home/semika/apache-tomcat-7.0.25/logs/mod_jk.log

# Set the jk log level [debug/error/info]
JkLogLevel info

# Select the log format
JkLogStampFormat "[%a %b %d %H:%M:%S %Y]"

# JkOptions indicate to send SSL KEY SIZE,
JkOptions +ForwardKeySize +ForwardURICompat -ForwardDirectories

# JkRequestLogFormat set the request format
JkRequestLogFormat "%w %V %T"

# Send everything for context /rainyDay to worker ajp13
JkMount /rainyDay ajp13
JkMount /rainyDay/* ajp13

I guess most of the properties defined above are clear for you. What is 'JkMount' and '/rainyDay'. The 'rainyDay' is one of my application deployed on Apache Tomcat. It says that "Forward all the request to the Apache Tomcat that are coming with /rainyDay namespace"

With that, we have finished the configuration of mod_jk. 

Now, We will test the environment. Try the following URL's.

http://localhost:8080/rainyDay

Nothing special about the above URL. Since I have deployed my 'rainyDay' application on Apache Tomcat, We can access the application even without configuring with Apache HTTP server using mod_jk. 

Now, try the following URL.

http://localhost:7000/rainyDay 

If you can access the application with above URL, our configuration is successful. We know that, We have not deployed the 'rainyDay' application on Apache HTTP server, but on Apache Tomcat and also Apache HTTP server is running on port 7000. We can still access the 'rainyDay' application deployed on Apache Tomcat via Apache HTTP server.

Now just try the following URL.  

https://localhost:7000/rainyDay 

With the above URL, you can not access the application since URL has https protocol. To access the application with https://, we need to configure SSL with Apache HTTP server.  

Configuring mod_ssl.

To enable SSL on Apache HTTP server, again you have to edit httpd.conf file. Open this file and look for the following line. 

# Secure (SSL/TLS) connections

#Include conf/extra/httpd-ssl.conf


Uncomment the above line and open it. That is under /httpd-2.2.22/conf/extra/httpd-ssl.conf. Look for the following properties.

SSLPassPhraseDialog  builtin
SSLEngine on
SSLCertificateFile "/home/semika/httpd-2.2.22/conf/server.crt"
SSLCertificateKeyFile "/home/semika/httpd-2.2.22/conf/server.key"

Uncomment, if some are already commented out. Next, we have to generate SSL certificate files, server.crt and server.key. To generate these file, execute the following commands.

cd /home/sermika/Downloads
openssl genrsa -des3 -out server.key 1024
openssl req -new -key server.key -out server.csr
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

When executing above commands, it will ask for some details and also a password and you have to provide those. Also, you have keep this password in mind, because it will be needed to provide when starting Apache HTTP server.

To know more about the configuring SSL with Apache HTTP server, refer the this documentation. Now carefully look into /home/semika/Downloads folder. You can see the server.key and server.crt generated. You have to copy these two files into Apache HTTP server installation directory.

cd /home/semika/Downloads
cp server.crt /home/semika/httpd-2.2.22/conf/server.crt
cp server.key /home/semika/httpd-2.2.22/conf/server.key

Again open the httpd-ssl.conf file under /httpd-2.2.22/conf/extra. You can see </VirtualHost> element and some properties are defined within that element. Like we did in mod_jk configuration, here also, we have to declare the required application context URL's or any other URL's that are needed to be secured with SSL. You have to add JkMount declaration as follows.

</VirtualHost>
    ...........
    ...........
    JkMount /rainyDay ajp13
    JkMount /rainyDay/* ajp13
</VirtualHost> 

Now try the following URL's

https://localhost:7000/

This should load the "It works" page. I used the port 7000, because I have change the Apache HTTP server default port. You are successfully configured mod_ssl. 

Now try the following URL as well.

https://localhost:7000/rainyDay

You should be able to load the application.  

Testing the environment.

There is no any order of starting Apache Tomcat and Apache HTTP server. After starting your servers, you can test your configuration with following URL's.

http://localhost:8080/
If you see, Tomcat's home page, Tomcat configuration is successful.

http://localhost:8080/rainyDay
If you can access the application, you application is successfully deployed on Apache Tomcat. 

http://localhost:7000/
If you see "It works" page, Apache HTTP server is successfully configured.

http://localhost:7000/rainyDay
If it loads you application, mod_jk configuration with Apache HTTP server is successful. 

https://localhost:7000/
Again if you see "It works" page, mod_ssl configuration with Apache HTTP server is success. 

https://localhost:7000/rainyDay
Again if you can access the application, mod_ssl configuration with Apache HTTP server is success and Apache HTTP server properly handle all secure requests to 'rainyDay' application successfully.

References:

  1. How to configure Apache HTTP server with mod_ssl. 
  2. How To Install Apache 2 with SSL on Linux (with mod_ssl, openssl). 
  3. The Apache Tomcat Connector - Webserver HowTo. 
  4. Apache Module mod_ssl. 
  5. configure - Configure the source tree.
Share

Widgets