Monday, July 18, 2011

Redirect after login to requested page with Spring after CSRF protection

Bookmarks, typing URLs directly in the address bar and getting to the requested page after login are functionalities you should not break as they impact user experience.

Once you have protected your Spring website against CSRF using the Synchronizer Token Pattern you will find that the redirection to the requested page after login functionality (You request a page, the login form shows up and after that you are taken to the page you originally requested) will be broken.

Basically the user might access a bookmark or just type a URL without the security token and the redirection will use the provided (and expired) security token or no security token at all. Of course you need to hook into Spring in order to change the default functionality.

First you use "authentication-success-handler-ref" form-login property in the Spring security context:
<beans:bean id="customAuthenticationHandler" class="com.nestorurquiza.web.handler.CustomAuthenticationHandler" />
    
<form-login login-page="/login"
            authentication-success-handler-ref="customAuthenticationHandler"
            authentication-failure-url="/login?error=authorizationFailed" />

Then implement the custom handler. Code should speak for itself.
package com.nestorurquiza.web.handler;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;

import com.nestorurquiza.utils.UrlTool;
import com.nestorurquiza.web.WebConstants;

public class CustomAuthenticationHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws ServletException, IOException {
        // TODO Auto-generated method stub
        String ctoken = (String) request.getSession().getAttribute(WebConstants.CSRF_TOKEN);
        DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST_KEY");
        if( defaultSavedRequest != null && ctoken != null ) {
            String requestUrl = defaultSavedRequest.getRequestURL() + "?" + defaultSavedRequest.getQueryString();
            requestUrl = UrlTool.addParamToURL(requestUrl, WebConstants.CSRF_TOKEN, ctoken, true);
            getRedirectStrategy().sendRedirect(request, response, requestUrl);
        } else {
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

Here is the little useful class that allows to override the ctoken parameter (or any other url parameter)
package com.nestorurquiza.utils;

public class UrlTool {
    public static String addParamToURL(String url, String param, String value,
            boolean replace) {
        if (replace == true)
            url = removeParamFromURL(url, param);
        return url + ((url.indexOf("?") == -1) ? "?" : "&") + param + "="
                + value;
    }

    public static String removeParamFromURL(String url, String param) {
        String sep = "&";
        int startIndex = url.indexOf(sep + param + "=");
        boolean firstParam = false;
        if (startIndex == -1) {
            startIndex = url.indexOf("?" + param + "=");
            if (startIndex != -1) {
                startIndex++;
                firstParam = true;
            }
        }

        if (startIndex != -1) {
            String startUrl = url.substring(0, startIndex);
            String endUrl = "";
            int endIndex = url.indexOf(sep, startIndex + 1);
            if(firstParam && endIndex != 1) {
                //remove separator from remaining url
                endUrl = url.substring(endIndex + 1);
            }
            return startUrl + endUrl;
        }

        return url;
    }

}

Wednesday, July 06, 2011

Caching with Spring ehcache and annotations

Update Oct 2012: In fact Spring supports now (JSR-107 AKA JCache although partially as the spec is not still ready as of 2012

Caching is trivial up to the moment you start wanting to cache too many entities. At that point you realize caching is actually a cross cutting concern which basically should be done the easy way, read using Inversion Of Control. But caching is also about where you cache: memory, file system?

If you are using Java then Spring in combination with ehCache can be used to provide caching while abstracting the developer from the details. To be able to achieve caching with minimum effort I recommend using ehcache spring annotations project. Note there is no need to add ehcache dependency as it is added by ehcache-spring-annotations project. Note that Spring 3.1 provides native support so probably it is a better idea to go that route.

Here are the dependencies I used for Spring 3.0.4:
<!-- ehcache -->
        <dependency>
            <groupId>com.googlecode.ehcache-spring-annotations</groupId>
            <artifactId>ehcache-spring-annotations</artifactId>
            <version>1.1.2</version>
            <type>jar</type>
        </dependency>

Create WEB-INF/ehcache.xml with the below content:
<?xml version="1.0" encoding="UTF-8"?>
  <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
      <defaultCache eternal="true" maxElementsInMemory="100" overflowToDisk="false" />
      <cache name="findAllClients" maxElementsInMemory="10000" eternal="true" overflowToDisk="false" />
  </ehcache>

In application context add the below lines:
<beans ...xmlns:ehcache="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring"...
...
xsi:schemaLocation="
...
http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.1.xsd
...
<!-- ehcache -->
    <ehcache:annotation-driven />
 
    <ehcache:config cache-manager="cacheManager">
        <ehcache:evict-expired-elements interval="60" />
    </ehcache:config>
 
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation"  value="/WEB-INF/ehcache.xml"/>
    </bean>
...

Look for a method that returns a collection of entities and annotate it like:
@Cacheable(cacheName = "findAllClients")
    public List<Client> findAll() {

If there is a method that inserts an entity related to the created cache (with name "findAllClients" in this case) then we annotate it so it cleans the cache after insertion:
@TriggersRemove(cacheName = "findAllClients", when = When.AFTER_METHOD_INVOCATION, removeAll = true)
    public void addClient(Client client) {

Of course there are cases where we are just consuming a collection let us say from a web service. We would like to force the cache to be cleaned even though we are never inserting an entity. Here is where you need to use a Controller that can be invoked to clean all or specific caches with a URL like:
http://localhost:8080/ehCache/remove?cacheName=findAllClients&cacheName=anotherCacheToRemove

Here is the Controller that will make this possible:
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/ehCache/*")
public class EhCacheController extends RootController {
    private static final String EHCACHE_SHOW_PATH = "/ehCache/show";
    
    @Autowired
    private CacheManager cacheManager;

    /**
     * Removes all caches if no parameter is passed
     * Removes all caches for the specified "cacheName" parameters
     * 
     * @param request
     * @param response
     * @return
     */
    
    @RequestMapping("/remove")
    public ModelAndView home(HttpServletRequest request, HttpServletResponse response) {
        ControllerContext ctx = new ControllerContext(request, response);
        init(ctx);
        
        String[] storedCacheNames = cacheManager.getCacheNames();
        String[] cacheNames = ctx.getParameterValues("cacheName");
        
        List<Cache> caches = new ArrayList<Cache> (storedCacheNames.length);
        
        for( String storedCacheName : storedCacheNames ){
            Cache storedCache = cacheManager.getCache(storedCacheName);
            if( cacheNames == null ) {
                storedCache.removeAll();
            } else {
                for( String cacheName : cacheNames ) {
                    if( cacheName.equalsIgnoreCase(storedCacheName) ) {
                        storedCache.removeAll();
                    }
                }
            }
            caches.add(storedCache);
        }
        
        ctx.setRequestAttribute("caches", caches);

        return getModelAndView(ctx, EHCACHE_SHOW_PATH);
    }
}

The show.jsp would be something like:
<%@ include file="/WEB-INF/jsp/includes.jsp" %>
<%@ include file="/WEB-INF/jsp/header.jsp" %>
<div class="global_error"><c:out value="${csrfError}" /></div>
<div><c:out value="${caches}" /></div>
<%@ include file="/WEB-INF/jsp/footer.jsp" %>

To add more caching you will need to add the cache entry in the ehcache.xml (thing that I do not like to be honest as I think the annotation should be parsed and if there is no declaration for it in XML then use just the default values or better allow customization as part of the annotation itself) and you need to annotate the method which return value will be cached. To clean that cache you can have either one or a combination of the @TriggersRemove annotation and a URL for cache cleaning like explained before.

Wednesday, June 29, 2011

The palest ink is better than the best memory

To document or not to document, that's the question.

When in doubt, write it out

Documentation saves hours of research and frustration and that translates into money. Yet so many IT managers tolerate systems without documentation while others go crazy after formalizations that end up in unmaintained material.

I still have to see documentation that is kept current. I am doing my best with my current team trying to enforce that practice. My opinion is documenting what you do is just about showing respect to your partners.

I am usually asked for the best anti-spyware or anti-virus for Windows and I always say: Rebuild your system every six months. Just have it documented: Basically keep a backup of your applications and related metadata and clear instructions as to how to recreate your system from scratch. Some years ago when I was using Windows as my main OS I found myself rebuilding my Windows box so often that I developed a procedure (part of it scripted I have to admit) that allowed me in just 2 hours to recreate my whole environment. That included: Apache, PHP, Java, Tomcat, JBoss, Eclipse, MS Office just to name few of them.

Nowadays I use VirtualBox and recovering Windows is just a matter of going to a safe Snapshot so the only reason for me to rebuild from scratch would be a company change (because of licensing)

The same happens in the world of OS virtualization. If you find that your P2V is difficult, go for a rebuild. With good documentation this could take anytime from 2 to 8 hours. Way less of what you will spend troubleshooting incompatibilities with your tools, different source and destination hardware etc. And if you do not have documentation you better start it.

This is exactly what happened to me when I tried to convert my Ubuntu 10 Server box into a VMware Server VM.

VMware converter will not work for Ubuntu 10. It is simply not supported http://www.vmware.com/support/converter/doc/releasenotes_conv40.html#platf

I tried anyway to install it (you know we always try just in case :-) but as you see below it is totally incompatible:

[several lines like below]
...
The script you are attempting to invoke has been converted to an Upstart
job, but lsb-header is not supported for Upstart jobs.
insserv: warning: script 'atd' missing LSB tags and overrides
insserv: Default-Start undefined, assuming empty start runlevel(s) for script `atd'
insserv: Default-Stop  undefined, assuming empty stop  runlevel(s) for script `atd'
The script you are attempting to invoke has been converted to an Upstart
job, but lsb-header is not supported for Upstart jobs.
insserv: warning: script 'udevmonitor' missing LSB tags and overrides
insserv: Default-Start undefined, assuming empty start runlevel(s) for script `udevmonitor'
insserv: Default-Stop  undefined, assuming empty stop  runlevel(s) for script `udevmonitor'
...
[more lines like above]

While I could have spent more time trying some other converters I decided to spend 8 hours building the box from scratch. But of course I had good old school documentation for building that box. WIKI is your fiend, don't forget that team member!

As the Chinese proverb says "The palest ink is better than the best memory"

Wednesday, June 22, 2011

Installing Configuring and Using Monit in Ubuntu

This is so straight forward and well documented in the Monit project that I wouldn't post about this if not just to allow those users that are starting with Linux and monitoring is their next concern.

The steps here should allow the user to install monit and monitor Google home page in no more than 5 minutes. It is always gratifying to get things running while following simple steps. Dealing with environment issues is the most difficult and frustrating part when you are trying to prove your concept.

  1. Install monit:
    sudo apt-get install monit
    
  2. Using sudo edit /etc/default/monit to setup automatic monit startup
    #startup=1
    START=yes
    
  3. Using sudo edit /etc/monit/monitrc as per the content below. Customize your web interface password, an IP allowed to connect via WEB (if you are a GUI guy), email addressee for alerts and mailserver. The rest of the settings should be OK for you. Note that the only check I am providing is checking Google availability just as a proof of concept. The web is full of samples to monitor anything in your servers
    ##################################################################
    
    # CUSTOM MONIT SETTINGS
    
    ##################################################################
    
    set mail-format {
    
      subject: [ $SERVICE ] $EVENT - $DATE
    
      message: Action: $ACTION, Description: $DESCRIPTION, Service: $SERVICE, Tested From Host: $HOST }
    
    # number of seconds between monit checks
    
    set daemon 60
    
    # Location of the monit log file
    
    # /var/log/messages is just ok
    
    # set logfile /var/log/monit.log
    
    set logfile syslog facility log_daemon
    
    # Change to the email address you want alerts to be sent to
    
    set alert nestorurquizaappmon@nestorurquiza.com
    
    # Port that the monit status page can be viewed
    
    set httpd port 2812
    
    # Allow localhost to connect
    
    allow localhost
    
    # The next line must be included to allow access from your computer
    
    # change the number to what your local ip is
    
    allow 0.0.0.0
    
    # The next line assigns a username:password combination to login.
    
    # Please change the password to something random.
    
    allow admin:myAdminPassword
    
    # Set the mailserver to send notification email.
    
    set mailserver mail.nestorurquiza.com
    
    ################################################################# 
    #REMOTE GOOGLE CHECKS
    ################################################################
    check host google-test with address google.com
      if failed port 80 proto http then alert
    group server
    
  4. Start, stop, restart, force reload as needed
    sudo /etc/init.d/monit start
    sudo /etc/init.d/monit stop
    sudo /etc/init.d/monit restart
    sudo /etc/init.d/monit force-reload
    
  5. See monit status, summary, unmonitor or monitor all as needed
    sudo monit status
    sudo monit summary
    sudo monit monitor all
    sudo monit unmonitor all
    
  6. Get extra help. Learn how to start or stop processes. Master monit!
    sudo monit -h
    
  7. If you prefer a GUI then get it with the below URL (Use the password you set)
    http://monit.server.address:2812/
    

Updating monit

Updating monit is easy. Just use this POB Recipe

Tuesday, June 21, 2011

ACL based security in JPA with jpasecurity: the next step after spring security

I had a clear need for ACL in my current project. Just protecting URLs is not enough and protecting method by method smells spaghetti code. Furthermore Spring solutions demand several hooks and still in my opinion they are still not addressing the real issue which is access control was removed from the database layer once ORM got mature but at the same time ORM did not provide a clean ACL solution.

If you are using JPA then you are in luck because jpasecurity project promises to resolve this limitation.

Rules are expressed in XML (I tested this so far) or Annotations (I had no luck with them so far). It allows granular access to CREATE, READ, UPDATE, DELETE operations based on roles and the current logged in user. It does that while wrapping all your JPA queries with the rules you specify. Basically you define access on let us say Client entity and every time Client entity appears in a JPA statement it wraps that statement adding the constraint. No need to say how powerful this is.

I used the trunk just because I was following up on some bugs that got corrected as I posted my questions. Probably you will get lucky and a stable 0.4.x release will be available by the time you decide to try it.

The examples here are based on a spring project using JPA + Hibernate + JTA + LDAP

Here are the main steps I followed:
  1. Edit your pom.xml
    <properties>
    ...
    <jpasecurity.version>0.4.0-SNAPSHOT</jpasecurity.version>
    ...
    </properties>
    ...
    <dependency>
              <groupId>net.sf.jpasecurity</groupId>
              <artifactId>jpasecurity-spring</artifactId>
              <version>${jpasecurity.version}</version>
              <exclusions>
               <exclusion>
                <artifactId>geronimo-ejb_3.1_spec</artifactId>
                <groupId>org.apache.geronimo.specs</groupId>
               </exclusion>
               <exclusion>
                <artifactId>geronimo-jpa_2.0_spec</artifactId>
                <groupId>org.apache.geronimo.specs</groupId>
               </exclusion>
              </exclusions>
    </dependency>
    ...
    
  2. If you need to debug some jpasecurity issues it is always useful to increase log level for the package
    log4j.logger.net.sf.jpasecurity=DEBUG
    
  3. I tried to use rules from annotations but the feature is still not well supported. I ended up putting my rules in XML. So here some samples. One of them as you can see quite complex:
    <security xmlns="http://jpasecurity.sf.net/xml/ns/security"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://jpasecurity.sf.net/xml/ns/security
                                  http://jpasecurity.sf.net/xml/ns/security/security_1_0.xsd">
    
      <persistence-unit name="nestorurquizaPersistenceUnit">
    <access-rule>GRANT CREATE READ        ACCESS TO Client c</access-rule>
    <access-rule>GRANT                    ACCESS TO Client c WHERE 'ROLE_ADMIN' IN (CURRENT_ROLES)</access-rule>
    <access-rule>GRANT ACCESS TO Client c WHERE c.id IN (SELECT cs.client.id FROM ClientStaffing cs, ClientStatus cst, Employee e WHERE e.email=CURRENT_PRINCIPAL AND cs.employee=e AND cs.client=c AND cs.endDate IS NULL AND ( cst.name &lt;&gt; 'Closed' OR cst.name IS NULL) )</access-rule>
      </persistence-unit>
    </security>
    
  4. In persistence.xml for your container:
    <!-- Comment the below if using jpasecurity -->
    <!--    <provider>net.sf.jpasecurity.persistence.SecurePersistenceProvider</provider>-->
    
        <!-- Uncomment the below if not using jpasecurity -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
    ...
    
        <properties>
                <!-- Comment the below if not using jpasecurity -->
    <!--            <property name="net.sf.jpasecurity.persistence.provider" value="org.hibernate.ejb.HibernatePersistence" />-->
    <!--            <property name="net.sf.jpasecurity.security.authentication.provider" value="com.nestorurquiza.security.JpasecuritySpringAuthenticationProvider"/>-->
    
  5. Note the need for a custom SpringAuthenticationProvider. This is just a hook to guarantee that CURRENT_PRINCIPAL maps to the user email which is the username in LDAP.
    package com.nestorurquiza.security;
    
    import net.sf.jpasecurity.spring.authentication.SpringAuthenticationProvider;
    
    import org.springframework.security.authentication.AnonymousAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    
    public class JpasecuritySpringAuthenticationProvider extends SpringAuthenticationProvider{
        @Override
        public Object getPrincipal() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null || (authentication instanceof AnonymousAuthenticationToken)) {
                return null;
            }
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            return userDetails.getUsername();
        }
    }
    
  6. I had to create a Custom Converter so Spring Binding works for forms. There is a bug I reported to Spring on this regard but anyway here is the workaround:
    package com.nestorurquiza.converter;
    
    import net.sf.jpasecurity.SecureObject;
    
    import org.springframework.core.convert.converter.Converter;
    
    public class SecureObjectToStringConverter implements Converter {
    
        @Override
        public String convert(SecureObject source) {
            return (source != null ? source.toString() : null);
        }
        
    }
    
  7. Then we need a custom ConversionServiceFactoryBean
    package com.nestorurquiza.converter;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.convert.support.GenericConversionService;
    import org.springframework.format.FormatterRegistry;
    import org.springframework.format.support.FormattingConversionServiceFactoryBean;
    
    /**
     * Not being used.
     * @author jia
     */
    public class CustomConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean {
    
        @Autowired
        private GenericConversionService genericConversionService;
        
        @Override
        protected void installFormatters(FormatterRegistry registry) {
            super.installFormatters(registry);
            //registry.addConverter(new BooleanToStringConverter());
            
            //Using org.springframework.format.support.FormattingConversionServiceFactoryBean from the xml declaration will not work
            //registry.addConverter(new SecureObjectToStringConverter());
            genericConversionService.addConverter(new SecureObjectToStringConverter());
        }
    }
    
    
  8. Then configure spring servlet with the necessary bean
    <!-- Registering custom ConversionService --> 
        <bean id="conversionService" class="com.nestorurquiza.converter.CustomConversionServiceFactoryBean" />
        <mvc:annotation-driven conversion-service="conversionService" />
        <!-- The below will not work at least for Binding --> 
        <!-- <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> 
            <property name="converters"> 
                <list> 
                    <bean class="com.nestorurquiza.converter.SecureObjectToStringConverter"/> 
                </list> 
            </property> 
        </bean> 
        -->
    
  9. Include the jpasecurity taglib for access rules in JSP
    <%-- Comment the below if not using jpasecurity --%>
    <%--@ taglib prefix="access" uri="http://jpasecurity.sf.net/access" --%>
    
  10. Use rules as needed in JSP
    <access:updating entity="client">
           <security:authorize url="/client/${client.id}/edit"><a href="<spring:url value="/client/${client.id}/edit?ctoken=${sessionScope.ctoken}"/>"><spring:message code="edit" /></a></security:authorize>
        </access:updating>
    
  11. Typical response when security is violated:
    java.lang.SecurityException: The current user is not permitted to update the specified object of type com.nestorurquiza.model.Client
    
  12. Some Jpasecurity limitations so far:
    • Does not accept CONCAT function so we must pass the percentages for the LIKE clauses within the parameters (which is best practice anyway)
    • count(*) is not supported. Use the entity alias instead like "SELECT count(c) FROM Client c"
    • LOWER and CONCAT functions are not supported but probably you can live without them and the less functions the best performance.
  13. Existing JUnit tests will fail if you enable jpasecurity and do not use a user with valid roles in the current context. The way you correct them is presented below:
    ...
    String adminEmail = "Admin.User@nestorurquiza.com";
    injectCurrentUser(adminEmail, Roles.ROLE_ADMIN);
    …
    private void injectCurrentUser(String email, String role) {
            TestingAuthenticationToken token = new TestingAuthenticationToken(
                    email, email, new GrantedAuthority[]{
                        new GrantedAuthorityImpl(role)});       
            SecurityContextHolder.getContext().setAuthentication(token);
    }
    

Sunday, June 19, 2011

Raw data and format in Jasper Reports

Raw data let us say a Float or a BigDecimal can be formatted in Excel following some patterns in a way that it is clearer for printing. However the raw data remains intact in a way that formulas can be applied later on those raw numbers. In accounting and financing this is a simple yet important concept to understand: separation between data and format.

JasperReports JRXML format uses its own formatting which is different from Excel and so when you want to export to Excel directly from iReport for example two things are going to happen:
  1. Probably you will have no formatting rule available and you will end up with an approximation of the real formatting you are after
  2. Your raw data could disappear and you get in the Excel output just exacty what the formatting in JRXML is specifying

When my attention was brought to this problem by a member of my current team I saw a post with some solutions I did not like.

The reason why I did not like the solutions (besides the fact they are not addressing the two points I described above) is that they basically violate separation of concerns.

The solution for this problem is indeed addressing the problem from the right angle.

It allows to format anything for non excel output from iReport (jrxml). It then relies on mapping to translate the custom jrxml format to the proprietary to Excel syntax when that format is needed.

I only came to that post after some research and the more I research the more convince I am people get easily lost nowadays when searching for solutions on the web. This is of course a result of a non semantic web markup and consequently a poor search engine filtering. Still there is something that can be done if you stick to concepts and you refine your terms following them.

Here is my personal story when I researched this issue. This could probably help others. Remember do never get the first response you get from Google as the final response to your problem:

  1. I found a similar issue within the JasperReports forums, not from Google. Then I asked if a solution was found

  2. I realized using JRXlsAbstractExporter was apparently the way to go.

  3. And so I finally got it.
  4. As this is a solution addressable only from the JR Excel API Exporter, if you are really stuck with POI Exporter then what to do? I could not find anything but as I knew from simple concept this was a task to be done by the exporter and the exporter uses the POI API then I searched for the necessary formatting and I got into this post

I feel like posting how you research is as useful as posting a solution. A good researcher cannot give up. A path must be follow till the end which is a Proof Of Concept (POC). There is no other way.

If you are the architect you must do that job. If you are the CTO be sure you do it yourself or have an architect to delegate to. If you are a developer and you do this you are on your way to become a good architect and a good hands on CTO.

Tuesday, June 14, 2011

Test Driven Bug Fixing for HTML Jasper Reports

Test Driven Bug Fixing (TDBF) is a must do as I have discussed before.

The structure of the site (HTML) can be effectively tested as we can assert the elements position (layout). The content can be asserted as we inspect the DOM. And that is just the beginning: Behaviour (javascript) and look and feel (CSS) can be tested as well.

Your reports might have a component of behavior and look and feel but the most important part to test is structure and content. Jasper Reports has several exporters and one of them is HTML. The HTML is absolute positioned and that creates the perfect scenario for testing.

If a bug has to be addressed then be sure you provide an automated test to ensure it will never come back.

All you need to do is:
  1. Business Analyst provides a test scenario where assertion on a cell content are made: The number for the current balance for this period for this account must be $1,000.00.
  2. Developer runs the Jasper Report from Firefox (Firebug plugin installed and if you want to get serious about it XPath Checker plugin installed as well) using HTML output.
  3. Developer runs the Jasper Report from Firefox using HTML output. He right click on the element and select "Inspect Element":
    <span style="position:absolute;left:388px;top:226px;width:72px;height:10px;text-align: right;">
    <span style="font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif; color: #000000; font-size: 8px;">100,000</span>
    </span>
    
  4. From Firebug you see right away a way to test both position and content. Here it is with XPATH:
    //span[@style="position:absolute;left:388px;top:226px;width:72px;height:10px;text-align: right;"]/span
    
  5. There is an advantage and a problem with the above.
    Advantage: It allows to assert not only the value but the position of the element
    Disadvantage: If the element absolute position is changed the test case will need to be changed. If we are interested in just asserting the data and not the absolute position of the element then the xpath is better retrieved from Firebug: Right click on the element on the firebug left pane and select "Copy XPath" to get something like:
    /html/body/table/tbody/tr/td[2]/div/span[71]/span
    
  6. Finally if you want to pleay with XPath a little bit more and assert really complicated scenarios start by right clicking something in the page content and selecting "View XPath". A new screen will come up from where you can see the xpath to get that element (be aware of the schema which you can remove for cleaner xpath expression). Go ahead and evaluate any random XPath from there as well.

Followers