Thursday, February 03, 2011

Hide context URL in Tomcat

Regardless if you use Apache plus modjk or any other way to separate the HTTP server from Tomcat (highly recommended in production environment) you will face the alternative between a nice looking URL like:
http://example.com/login
versus
http://example.com/my-app/login

For this to work you will need to configure the Host node in server.xml as shown below. Notice the full path which is required and makes sense as this is an environment specific setting.
...
      <Host name="bhub.nestorurquiza.com" debug="0">
                <Context path=""
                         docBase="/opt/tomcat/webapps/my-app"/>
      </Host>
    </Engine>
  </Service>
</Server>

Reloading log4j properties in Tomcat

I would love to see on demand log4j properties reloading in application servers.

If you want to reload log4j configuration it is because you have made a change let us say to debug a specific issue appearing right now in production. Is there a real need to be monitoring the file for changes? Wouldn't it be better to use resources just by the time we really want a reload?

Tomcat provides a mechanism to allow dynamic reloading and JBoss ships with the solution already integrated (meaning the standard installation will check the file to reload it every so often)

The point is they do not provide a simple admin link to force reloading which in my opinion goes against the natural process: change configuration file, save it and expect the changes. Just in order to avoid a final step "push the changes" is not enough in my opinion. Apache Server knows that very well, you can reload your configuration but on demand.

On demand reloading has a big advantage: You use resources just when you need them. With that approach we could avoid performance problems: read using "reloadable" for in production for tomcat 7 or memory leaks which I have to say have hit my teams both in JBoss and Tomcat environments.

I see no big deal on allowing an on demand reloading but while I wait for that day I prefer to reload the application context after changing the log4j properties file:
$ vi tomcat/webapps/my-app/WEB-INF/classes/log4j.properties 
$ touch tomcat/webapps/my-app/WEB-INF/web.xml 

And yes you must do this with caution in production as you kick out some of your users from that node in your cluster or even worst, you could be putting your service down for some seconds (Yes Tomcat is extremely fast especially when you avoid complicated processes at "Servlet Listener" time)

This is clearly far from ideal as the issue you are trying to debug might not be happening again after you restart your application. So think twice before deciding to increase log level in production and think more about creating load test scenarios in integration and staging environments.

Monday, January 31, 2011

Current page or resource in J2EE servlet application

Every so often I get the same question on my plate: How to find the current invoked resource or page in a J2EE servlet application. Hopefully the examples below go straight to what you need:
((HttpServletRequest)request).getRequestURL().toString()
http://sample.com/sample-app/css/main.css

((HttpServletRequest)request).getRequestURI()
/sample-app/css/main.css

((HttpServletRequest)request).getSession().getServletContext().getRealPath(((HttpServletRequest)request).getContextPath())
/Users/nestor/tomcat/webapps/sample-app/css/main.css

Friday, January 28, 2011

Remember Me with LDAP Spring Security

Remember Me functionality in Spring Security can be as easy as implement as just using a checkbox in your HTML ...
<input type='checkbox' name='_spring_security_remember_me' value="on"/>

... plus a declaration in the spring application context xml file:
<remember-me/>

However that is not true when using LDAP

For LDAP a persistent token will be needed which is actually good news as it is more secure. Here are the important bits to ensure the system remembers the user for 4 hours:
<http auto-config="true" use-expressions="true" access-decision-manager-ref="accessDecisionManager">
...
   <remember-me key="_spring_security_remember_me" token-validity-seconds="14400" token-repository-ref="tokenRepository"/>
A UserService is needed:
<ldap-user-service id="ldapUserService" group-search-base="ou=groups,o=MyCompany" group-role-attribute="cn" group-search-filter="(uniquemember={0})" user-search-base="ou=people,o=MyCompany" user-search-filter="mail={0}"/>
And a token Repository:
<beans:bean id="tokenRepository"
      class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
      <beans:property name="createTableOnStartup" value="false" />
      <beans:property name="dataSource" ref="myDataSource"/>
   </beans:bean>
Note createTableOnStartup can be true the first time you start your application so it creates the "persistent_logins" table automatically. Below is the schema creation statement for MySQL. You can just run the statement and leave createTableOnStartup=false.
CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

Now spring will keep track of the last time the user logs in and will remember the user as long as the token-validity-seconds is not reached.

If you are protecting your site against CSRF attacks which you should of course then you will face an additional problem: The user will be remembered but the security token will be invalid.

To address that issue you could use a combination of before and after security filters or just a before filter. Basically you can put any code you want before (or after) the remember me functionality is triggered. Here the relevant xml bits:
<beans:bean id="customRememberMeFilter" class="com.nestorurquiza.web.filter.CustomRememberMeFilter" />
...
<http auto-config="true" use-expressions="true" access-decision-manager-ref="accessDecisionManager">
...
   <custom-filter  before="REMEMBER_ME_FILTER" ref="customRememberMeFilter" />

And here is the filter:
package com.nestorurquiza.web.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import com.nestorurquiza.web.ControllerContext;

public class CustomRememberMeFilter implements Filter 
{
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
    {
        Authentication authentication = (Authentication) SecurityContextHolder.getContext().getAuthentication();
        String ctoken = request.getParameter(ControllerContext.CSRF_TOKEN);
        HttpServletRequest req = (HttpServletRequest)request;
        if(authentication == null && ctoken != null && getRememberMeCookie(req.getCookies()) != null) {
            req.getSession().setAttribute(ControllerContext.CSRF_TOKEN, ctoken);
        }
        chain.doFilter(request, response);
    }

    public void destroy() 
    {   

    }

    public void init(FilterConfig config) throws ServletException 
    {
    
    }
    
    private Cookie getRememberMeCookie(Cookie[] cookies) {
        if (cookies != null)
          for (Cookie cookie : cookies) {
            if (AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY.equals(cookie.getName())) {
              return cookie;
            }
          }
        return null;
    }
}

While remember me functionality is very handy in some circumstances it is abused by some developers who ignore the vulnerabilities behind this feature.

AS you can see from the above code we are disabling CSRF protection for users opting to be remembered.

Here are some measures:
  1. Avoid if possible using Remember Me functionality
  2. Explain your users they will be more vulnerable
  3. Instruct your users to log-out if they opted to use remember-me functionality but have finished their job. Closing the browser is not enough to prevent a different user from gaining control over the original account

Sunday, January 23, 2011

Liferay Agile JSP development with Maven

If you use Maven for Liferay SDK Plugin development you can get your JSPs changes instantaneously in the browser. This involves two steps:
  1. Create an xml file named for example my-portlet.xml and copy it to the Liferay deploy folder. The content of the file is a single xml node with attribute "docBase" which value must be the target exploded application directory. For example:
    <Context
       docBase="/Users/nestor/projects/my-portlet/target/my-portlet"
       />
    
  2. You need to make sure when you change your JSPs they are automatically pushed into the target directory. This is the default behaviour in Netbeans. In Eclipse you can play with the Maven plugin for this however there is a fastest way: Use the fileSync eclipse plugin

You might be wondering why you need to use a context file while you don't need it when doing servlet programming.

The answer is that Liferay will manage web applications in specific folders below the temp directory. The folders have a portlet number followed by a dash and then the name of the portlet. Under those folders is where you need to touch JSP files in order for the changes to show up. However the number might change so the folder path is not a constant. Definitely the "/Context@docBase" directive comes in really handy.

Saturday, January 22, 2011

Load Tests with Jmeter: Liferay Example

If you build a WEB application regardless the programming language or framework you must test how it will perform under real world load:
  1. Concurrency problems will not be picked by code reviews, best practices, programming to interfaces, TDD (You could but I am wonder who would be able to afford it), agile project management techniques etc.
  2. You must be prepared for your expected traffic and in fact you should write an email to your supervisors making them aware of the software limitation: We currently can handle x concurrent users.
  3. Formulas and calculations are OK for estimatives but that is just the theory. Do your hoework and be sure your software will be able to handle the expected traffic.
Jakarta JMeter is your free open source friend.

I have decided to use Liferay to show an example of a load test. You just need to download Jmeter unzip it and run jmeter from the bin folder, open then the file I pasted below and edit the "Http Request Defaults" to point to your Liferay instance. Then verify how your server will handle 100 concurrent users using a ramp up period of 10 seconds.

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="2.1">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">100</stringProp>
        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
        <longProp name="ThreadGroup.start_time">1289581012000</longProp>
        <longProp name="ThreadGroup.end_time">1289581012000</longProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSampler guiclass="HttpTestSampleGui" testclass="HTTPSampler" testname="/web/guest" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
            <collectionProp name="Arguments.arguments"/>
          </elementProp>
          <stringProp name="HTTPSampler.domain"></stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/web/guest</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.monitor">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
        </HTTPSampler>
        <hashTree>
          <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
            <collectionProp name="HeaderManager.headers">
              <elementProp name="Accept-Language" elementType="Header">
                <stringProp name="Header.name">Accept-Language</stringProp>
                <stringProp name="Header.value">en-us,en;q=0.5</stringProp>
              </elementProp>
              <elementProp name="Accept" elementType="Header">
                <stringProp name="Header.name">Accept</stringProp>
                <stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8</stringProp>
              </elementProp>
              <elementProp name="Keep-Alive" elementType="Header">
                <stringProp name="Header.name">Keep-Alive</stringProp>
                <stringProp name="Header.value">115</stringProp>
              </elementProp>
              <elementProp name="User-Agent" elementType="Header">
                <stringProp name="Header.name">User-Agent</stringProp>
                <stringProp name="Header.value">Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 GTB7.1</stringProp>
              </elementProp>
              <elementProp name="Accept-Encoding" elementType="Header">
                <stringProp name="Header.name">Accept-Encoding</stringProp>
                <stringProp name="Header.value">gzip,deflate</stringProp>
              </elementProp>
              <elementProp name="Accept-Charset" elementType="Header">
                <stringProp name="Header.name">Accept-Charset</stringProp>
                <stringProp name="Header.value">ISO-8859-1,utf-8;q=0.7,*;q=0.7</stringProp>
              </elementProp>
            </collectionProp>
          </HeaderManager>
          <hashTree/>
        </hashTree>
        <HTTPSampler guiclass="HttpTestSampleGui" testclass="HTTPSampler" testname="/html/js/barebone.jsp" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
            <collectionProp name="Arguments.arguments">
              <elementProp name="browserId" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">browserId</stringProp>
                <stringProp name="Argument.value">firefox</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="themeId" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">themeId</stringProp>
                <stringProp name="Argument.value">005E82_WAR_005E82theme</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="colorSchemeId" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">colorSchemeId</stringProp>
                <stringProp name="Argument.value">01</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="minifierType" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">minifierType</stringProp>
                <stringProp name="Argument.value">js</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="minifierBundleId" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">minifierBundleId</stringProp>
                <stringProp name="Argument.value">javascript.barebone.files</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="t" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">t</stringProp>
                <stringProp name="Argument.value">1274823175000</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain"></stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/html/js/barebone.jsp</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.monitor">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
        </HTTPSampler>
        <hashTree>
          <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
            <collectionProp name="HeaderManager.headers">
              <elementProp name="Accept-Language" elementType="Header">
                <stringProp name="Header.name">Accept-Language</stringProp>
                <stringProp name="Header.value">en-us,en;q=0.5</stringProp>
              </elementProp>
              <elementProp name="Accept" elementType="Header">
                <stringProp name="Header.name">Accept</stringProp>
                <stringProp name="Header.value">*/*</stringProp>
              </elementProp>
              <elementProp name="Keep-Alive" elementType="Header">
                <stringProp name="Header.name">Keep-Alive</stringProp>
                <stringProp name="Header.value">115</stringProp>
              </elementProp>
              <elementProp name="User-Agent" elementType="Header">
                <stringProp name="Header.name">User-Agent</stringProp>
                <stringProp name="Header.value">Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 GTB7.1</stringProp>
              </elementProp>
              <elementProp name="Accept-Encoding" elementType="Header">
                <stringProp name="Header.name">Accept-Encoding</stringProp>
                <stringProp name="Header.value">gzip,deflate</stringProp>
              </elementProp>
              <elementProp name="Accept-Charset" elementType="Header">
                <stringProp name="Header.name">Accept-Charset</stringProp>
                <stringProp name="Header.value">ISO-8859-1,utf-8;q=0.7,*;q=0.7</stringProp>
              </elementProp>
            </collectionProp>
          </HeaderManager>
          <hashTree/>
        </hashTree>
        <HTTPSampler guiclass="HttpTestSampleGui" testclass="HTTPSampler" testname="/image/company_logo" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
            <collectionProp name="Arguments.arguments">
              <elementProp name="img_id" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">img_id</stringProp>
                <stringProp name="Argument.value">10303</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="amp;t" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">amp;t</stringProp>
                <stringProp name="Argument.value">1289581656500</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain"></stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/image/company_logo</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.monitor">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
        </HTTPSampler>
        <hashTree>
          <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
            <collectionProp name="HeaderManager.headers">
              <elementProp name="Accept-Language" elementType="Header">
                <stringProp name="Header.name">Accept-Language</stringProp>
                <stringProp name="Header.value">en-us,en;q=0.5</stringProp>
              </elementProp>
              <elementProp name="Accept" elementType="Header">
                <stringProp name="Header.name">Accept</stringProp>
                <stringProp name="Header.value">image/png,image/*;q=0.8,*/*;q=0.5</stringProp>
              </elementProp>
              <elementProp name="Keep-Alive" elementType="Header">
                <stringProp name="Header.name">Keep-Alive</stringProp>
                <stringProp name="Header.value">115</stringProp>
              </elementProp>
              <elementProp name="User-Agent" elementType="Header">
                <stringProp name="Header.name">User-Agent</stringProp>
                <stringProp name="Header.value">Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 GTB7.1</stringProp>
              </elementProp>
              <elementProp name="Accept-Encoding" elementType="Header">
                <stringProp name="Header.name">Accept-Encoding</stringProp>
                <stringProp name="Header.value">gzip,deflate</stringProp>
              </elementProp>
              <elementProp name="Accept-Charset" elementType="Header">
                <stringProp name="Header.name">Accept-Charset</stringProp>
                <stringProp name="Header.value">ISO-8859-1,utf-8;q=0.7,*;q=0.7</stringProp>
              </elementProp>
            </collectionProp>
          </HeaderManager>
          <hashTree/>
        </hashTree>
        <HTTPSampler guiclass="HttpTestSampleGui" testclass="HTTPSampler" testname="/web/guest/home?p_p_id=58&amp;p_p_lifecycle=1&amp;p_p_state=normal&amp;p_p_mode=view&amp;p_p_col_id=column-1&amp;p_p_col_count=1&amp;saveLastPath=0&amp;_58_struts_action=%2Flogin%2Flogin" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
            <collectionProp name="Arguments.arguments">
              <elementProp name="_58_redirect" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">_58_redirect</stringProp>
                <stringProp name="Argument.value"></stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
              </elementProp>
              <elementProp name="_58_rememberMe" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">_58_rememberMe</stringProp>
                <stringProp name="Argument.value">on</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
              </elementProp>
              <elementProp name="_58_login" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">_58_login</stringProp>
                <stringProp name="Argument.value">nurquiza@sample.com</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
              </elementProp>
              <elementProp name="_58_password" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.name">_58_password</stringProp>
                <stringProp name="Argument.value">test</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain"></stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/web/guest/home?p_p_id=58&amp;p_p_lifecycle=1&amp;p_p_state=normal&amp;p_p_mode=view&amp;p_p_col_id=column-1&amp;p_p_col_count=1&amp;saveLastPath=0&amp;_58_struts_action=%2Flogin%2Flogin</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.monitor">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
        </HTTPSampler>
        <hashTree>
          <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
            <collectionProp name="HeaderManager.headers">
              <elementProp name="Content-Type" elementType="Header">
                <stringProp name="Header.name">Content-Type</stringProp>
                <stringProp name="Header.value">application/x-www-form-urlencoded</stringProp>
              </elementProp>
              <elementProp name="Accept-Language" elementType="Header">
                <stringProp name="Header.name">Accept-Language</stringProp>
                <stringProp name="Header.value">en-us,en;q=0.5</stringProp>
              </elementProp>
              <elementProp name="Accept" elementType="Header">
                <stringProp name="Header.name">Accept</stringProp>
                <stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8</stringProp>
              </elementProp>
              <elementProp name="Keep-Alive" elementType="Header">
                <stringProp name="Header.name">Keep-Alive</stringProp>
                <stringProp name="Header.value">115</stringProp>
              </elementProp>
              <elementProp name="User-Agent" elementType="Header">
                <stringProp name="Header.name">User-Agent</stringProp>
                <stringProp name="Header.value">Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 GTB7.1</stringProp>
              </elementProp>
              <elementProp name="Accept-Encoding" elementType="Header">
                <stringProp name="Header.name">Accept-Encoding</stringProp>
                <stringProp name="Header.value">gzip,deflate</stringProp>
              </elementProp>
              <elementProp name="Accept-Charset" elementType="Header">
                <stringProp name="Header.name">Accept-Charset</stringProp>
                <stringProp name="Header.value">ISO-8859-1,utf-8;q=0.7,*;q=0.7</stringProp>
              </elementProp>
            </collectionProp>
          </HeaderManager>
          <hashTree/>
        </hashTree>
      </hashTree>
      <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
        <boolProp name="ResultCollector.error_logging">false</boolProp>
        <objProp>
          <name>saveConfig</name>
          <value class="SampleSaveConfiguration">
            <time>true</time>
            <latency>true</latency>
            <timestamp>true</timestamp>
            <success>true</success>
            <label>true</label>
            <code>true</code>
            <message>true</message>
            <threadName>true</threadName>
            <dataType>true</dataType>
            <encoding>false</encoding>
            <assertions>true</assertions>
            <subresults>true</subresults>
            <responseData>false</responseData>
            <samplerData>false</samplerData>
            <xml>true</xml>
            <fieldNames>false</fieldNames>
            <responseHeaders>false</responseHeaders>
            <requestHeaders>false</requestHeaders>
            <responseDataOnError>false</responseDataOnError>
            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
            <assertionsResultsToSave>0</assertionsResultsToSave>
            <bytes>true</bytes>
          </value>
        </objProp>
        <stringProp name="filename"></stringProp>
      </ResultCollector>
      <hashTree/>
      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
        <collectionProp name="CookieManager.cookies"/>
        <boolProp name="CookieManager.clearEachIteration">false</boolProp>
        <stringProp name="CookieManager.policy">rfc2109</stringProp>
      </CookieManager>
      <hashTree/>
      <ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true">
        <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
          <collectionProp name="Arguments.arguments"/>
        </elementProp>
        <stringProp name="HTTPSampler.domain">portal.sample.com</stringProp>
        <stringProp name="HTTPSampler.port">443</stringProp>
        <stringProp name="HTTPSampler.connect_timeout"></stringProp>
        <stringProp name="HTTPSampler.response_timeout"></stringProp>
        <stringProp name="HTTPSampler.protocol">https</stringProp>
        <stringProp name="HTTPSampler.contentEncoding"></stringProp>
        <stringProp name="HTTPSampler.path"></stringProp>
      </ConfigTestElement>
      <hashTree/>
      <ResultCollector guiclass="TableVisualizer" testclass="ResultCollector" testname="View Results in Table" enabled="true">
        <boolProp name="ResultCollector.error_logging">false</boolProp>
        <objProp>
          <name>saveConfig</name>
          <value class="SampleSaveConfiguration">
            <time>true</time>
            <latency>true</latency>
            <timestamp>true</timestamp>
            <success>true</success>
            <label>true</label>
            <code>true</code>
            <message>true</message>
            <threadName>true</threadName>
            <dataType>true</dataType>
            <encoding>false</encoding>
            <assertions>true</assertions>
            <subresults>true</subresults>
            <responseData>false</responseData>
            <samplerData>false</samplerData>
            <xml>true</xml>
            <fieldNames>false</fieldNames>
            <responseHeaders>false</responseHeaders>
            <requestHeaders>false</requestHeaders>
            <responseDataOnError>false</responseDataOnError>
            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
            <assertionsResultsToSave>0</assertionsResultsToSave>
            <bytes>true</bytes>
          </value>
        </objProp>
        <stringProp name="filename"></stringProp>
      </ResultCollector>
      <hashTree/>
      <ResultCollector guiclass="GraphVisualizer" testclass="ResultCollector" testname="Graph Results" enabled="true">
        <boolProp name="ResultCollector.error_logging">false</boolProp>
        <objProp>
          <name>saveConfig</name>
          <value class="SampleSaveConfiguration">
            <time>true</time>
            <latency>true</latency>
            <timestamp>true</timestamp>
            <success>true</success>
            <label>true</label>
            <code>true</code>
            <message>true</message>
            <threadName>true</threadName>
            <dataType>true</dataType>
            <encoding>false</encoding>
            <assertions>true</assertions>
            <subresults>true</subresults>
            <responseData>false</responseData>
            <samplerData>false</samplerData>
            <xml>true</xml>
            <fieldNames>false</fieldNames>
            <responseHeaders>false</responseHeaders>
            <requestHeaders>false</requestHeaders>
            <responseDataOnError>false</responseDataOnError>
            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
            <assertionsResultsToSave>0</assertionsResultsToSave>
            <bytes>true</bytes>
          </value>
        </objProp>
        <stringProp name="filename"></stringProp>
      </ResultCollector>
      <hashTree/>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

There are a few important things you need to know to keep a Jmeter test growing and usable for your needs.
  1. Thread Group: Besides simulating a specific number of users do use a ramp up period that makes sense. Any system even Google can be a victim of denial of service so do test real non-attack case scenarios. This means it is unlikely (if not practically impossible) the 100 users end up hitting your server at the same time. As you see I have distributed the load in 10 seconds.
  2. See how I include pulling resources like the logo besides hitting the home page and trying to login into the website. Resources are cached so it is up to you to decide if they should be included in a stress test. For example if you launched a marketing campaign and your users will be new all resources will be downloaded. Contrary to the common core developers believe front end code is usually the bottleneck in many web applications
  3. Always use a "Default Requests Defaults Config Element" so you can easily use the same test against your local environment and real servers (Be sure you do not hit real production during high traffic hours or you will end up affecting real users)
  4. Look into the server logs looking for exceptions
  5. Think about simulating cases where local sample data is affected by several different users. In this case I have added only one user but you can use expressions and build different users on the fly or harcode them in different threads. Analyze the data after running the tests looking for inconsistencies.
  6. Use a Cookie Manager so you maintain the session between requests
  7. Run the test first with one user and analyze all responses. There are several views, the "View Results Tree Listener" is your friend to see request and response of each request.
  8. Use a "View Results in Table Listener" for a quick inspection about response delays. It helps your identify botlenecks.
  9. Use Graph results to get statistics. Be sure you understand what average, median and deviation is. Use a larger user base for example 1000 users distributed through 2 minutes or a little more to be sure the statistics have a better meaning. The more samples the better as the server will be warmed up simulating better those enjoying a large uptime
  10. Use a random timer after each request. My example does not include them but you can easily add it from Menu | Edit | Add. That ensures a closer to reality scenario. A user will not be hitting every second your website.
  11. Simulate a denial of service attack and see if you are prepared for it in the case you are not handling that at firewall layer (recommended)
  12. There is so much more you can do including creating the test cases from a normal browser navigation using a Proxy

The SDLC is a long circle and testing is an indispensable part of it. Stress tests or load tests are important, way more than what many developers think. Take control and stress the application, don't let the application stress yourself.

Tuesday, January 18, 2011

Liferay MVC with JSPPortlet

JSPPortlet can be used to quickly develop a Liferay SDK plugin type portlet or even as I demonstrated before migrate one of the existing native portlets to the more agile plugin environment.

My example for the update password portlet was actually still using the original Struts action (UpdatePasswordAction.java).

When further customization is needed we face a problem: Parameters sent to one portlet cannot be retrieved from a second portlet. This is simply correct for a JSR-168 specification implementation. You could go around this issue using JSR-286 public render parameters but there is an alternative which in this case is actually cleaner: Use JSPPortlet MVC capabilities to migrate the extension environment struts portlet java code to a plugin environment implementation.

So let us change then the portlet to show a results page (view) when the passwordis changed instead of keeping the form (edit).

I have committed the code to SVN but for the sake of clarity here are the changes I have made to the Mavenized SDK Plugin Update Password Portlet:
  1. Use two JSPs (view and edit)
            <init-param>
                <name>view-jsp</name>
                <value>/update_password_view.jsp</value>
            </init-param>
            <init-param>
                <name>edit-jsp</name>
                <value>/update_password_edit.jsp</value>
            </init-param>
    
  2. Make UpdatePasswordJSPPortlet inherit from the typical com.sample.jsp.portlet.JSPPortlet. The code should be self-explanatory
    package com.sample.jsp.portlet;
    
    import java.io.IOException;
    
    import javax.portlet.ActionRequest;
    import javax.portlet.ActionResponse;
    import javax.portlet.PortletException;
    import javax.portlet.PortletSession;
    import javax.portlet.RenderRequest;
    import javax.portlet.RenderResponse;
    import javax.servlet.http.HttpServletRequest;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    import com.liferay.portal.NoSuchUserException;
    import com.liferay.portal.UserPasswordException;
    import com.liferay.portal.kernel.servlet.SessionErrors;
    import com.liferay.portal.kernel.util.ParamUtil;
    import com.liferay.portal.kernel.util.Validator;
    import com.liferay.portal.security.auth.PrincipalException;
    import com.liferay.portal.service.UserServiceUtil;
    import com.liferay.portal.util.PortalUtil;
    
    public class UpdatePasswordJSPPortlet extends JSPPortlet {
        private static final String CMD = "cmd";
    
        @Override
        public void doDispatch(
                RenderRequest renderRequest, RenderResponse renderResponse)
                throws IOException, PortletException {
            HttpServletRequest request = PortalUtil.getHttpServletRequest(renderRequest);
            HttpServletRequest originalRequest = PortalUtil.getOriginalServletRequest(request);
    
            String cmd = originalRequest.getParameter(CMD);
            if(Validator.isNull(cmd)) {
                include(editJSP, renderRequest, renderResponse);
            } else {
                if(SessionErrors.isEmpty(request)) {
                    include(viewJSP, renderRequest, renderResponse);
                }else{
                    include(editJSP, renderRequest, renderResponse);
                }
            }
        }
    
        
        @Override
        public void processAction(ActionRequest actionRequest,
            ActionResponse actionResponse) throws IOException, PortletException {
            
            String cmd = ParamUtil.getString(actionRequest, CMD);
    
            if (Validator.isNull(cmd)) {
                    actionResponse.setRenderParameter("error", "cmd is null");
                    return;
            }
            
            HttpServletRequest request = PortalUtil.getHttpServletRequest(actionRequest);
            SessionErrors.clear(request);
            try {
                    updatePassword(actionRequest, actionResponse);
                    return;
            }
            catch (Exception e) {
                    if (e instanceof UserPasswordException) {
                            SessionErrors.add(request, e.getClass().getName(), e);
                    }
                    else if (e instanceof NoSuchUserException ||
                                     e instanceof PrincipalException) {
                            SessionErrors.add(request, e.getClass().getName());
                    }
                    else {
                            PortalUtil.sendError(e, actionRequest, actionResponse);
                    }
            }
        }
        
        private void updatePassword(ActionRequest request, ActionResponse response)
                throws Exception {
    
            PortletSession session = request.getPortletSession();
    
            long userId = PortalUtil.getUserId(request);
            String password1 = ParamUtil.getString(request, "password1");
            String password2 = ParamUtil.getString(request, "password2");
            boolean passwordReset = false;
    
            UserServiceUtil.updatePassword(userId, password1, password2,
                    passwordReset);
    
            session.setAttribute("USER_PASSWORD", password1);
        }
    
        private static final Log log = LogFactory.getLog(UpdatePasswordJSPPortlet.class);
    }
    
    

Followers