Thursday, March 26, 2015

Integration Testing with SoapUI, Maven, Cargo, JaCoCo

Integration Testing RESTful Java Application

As a part of our Continous Delivery plan we wanted to have integration tests exercise our java code served.  The choosen means of integration testing was SoapUI which provides a advanced gui for setting up requests from wadl and generating tests from that.  Our goal was to have maven run soapui tests during the "integration-test" phase.  A code coverage report will help establish the quality of the testing beyond just know that X number tests were executed.



Maven

Using maven all these tools can be integrated to run right after build and succeed or fail the deployment/installation.  This is a very important part of the Continuous Delivery plan incorporating a fail-fast technique so broken code doesn't get past on to roll-outs.  Individual developers can also check their changed prior to check-in.

SoapUI

SoapUI was select as the tool for integration tests because is was useable by more then the developers in the organization.  The tests could be ported and expanded from development to QA and more widely used throughout the organization.  Also the required inputs would be documented by development because they would be in the test. 
<plugin>
                <groupId>com.smartbear.soapui</groupId>
                <artifactId>soapui-pro-maven-plugin</artifactId>
                <version>5.1.2</version>
                <configuration>
                    <projectFile>${basedir}/src/test/soapui/soapui-test.xml</projectFile>
                    <outputFolder>${basedir}/target/soapui-reports/</outputFolder>
                    <junitReport>true</junitReport>
                    <endpoint>http://localhost:9091</endpoint>
                    <skip>${skipITs}</skip>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.reflections</groupId>
                        <artifactId>reflections</artifactId>
                        <version>0.9.9-RC1</version>
                    </dependency>
                    <dependency>
                        <groupId>org.apache.poi</groupId>
                        <artifactId>poi-ooxml</artifactId>
                        <version>3.10-FINAL</version>
                        <exclusions>
                            <exclusion>
                                <groupId>org.apache.xmlbeans</groupId>
                                <artifactId>xmlbeans</artifactId>
                            </exclusion>
                        </exclusions>
                    </dependency>
                </dependencies>
                <executions>
                    <execution>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

JaCoCo Javaagent http://www.eclemma.org/jacoco/

Jacoco monitors the JVM and analyses the code coverage of the tests.  One thing to note because new frameworks like spring proxy classes during runtime code jacoco can't follow the code coverage into proxy classes because classid get confused.  See link

<plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.7.2.201409121644</version>
                <configuration>
                    <destFile>${jacoco.reportPath}</destFile>
                    <dataFile>${jacoco.reportPath}</dataFile>
                    <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
                    <classDumpDir>${project.reporting.outputDirectory}/jacoco-it/classes</classDumpDir>
                    <skip>${skipITs}</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>jacoco-agent</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                        <configuration>
                            <destFile>${jacoco.reportPath}</destFile>
                            <propertyName>jacoco.agent.itArgLine</propertyName>
                        </configuration>
                    </execution>
                    <execution>
                        <id>jacoco-report</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>dump</goal>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

Cargo (Tomcat 7)

I had to use cargo over the maven-tomcat7 plugin because cargo allows you to inject the jacoco java agent.
<plugin>
                <groupId>org.codehaus.cargo</groupId>
                <artifactId>cargo-maven2-plugin</artifactId>
                <version>1.4.11</version>
                <configuration>
                    <skip>${skipITs}</skip>
                    <container>
                        <containerId>tomcat7x</containerId>
                        <zipUrlInstaller>
                            <url>http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.59/bin/apache-tomcat-7.0.59.zip</url>
                            <downloadDir>${project.build.directory}/downloads</downloadDir>
                            <extractDir>${project.build.directory}/extracts</extractDir>
                        </zipUrlInstaller>
                        <dependencies>
                            <dependency>
                                <groupId>oracle</groupId>
                                <artifactId>ojdbc6</artifactId>
                            </dependency>
                        </dependencies>
                    </container>
                    <configuration>
                        <home>${project.build.directory}/catalina-base</home>
                        <properties>
                            <cargo.jvmargs>${jacoco.agent.itArgLine},output=tcpserver,port=6300 -Drunmode=TEST</cargo.jvmargs>
                            <cargo.servlet.port>9091</cargo.servlet.port>
                            <cargo.tomcat.ajp.port>9100</cargo.tomcat.ajp.port>
                        </properties>
                        <configfiles>
                            <configfile>
                                <file>${basedir}/src/test/conf/context.xml</file>
                                <todir>conf/Catalina/localhost/</todir>
                                <tofile>context.xml.default</tofile>
                            </configfile>
                        </configfiles>
                    </configuration>
                </configuration>
                <executions>
                    <execution>
                        <id>start-tomcat</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>start</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>stop-tomcat</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>


Sunday, March 1, 2015

jQuery Datatable Remote pagination with Spring MVC / SpringData


So I wanted to use the nice jQuery Datatable and integrate it with Spring-Boot backend using Spring MVC Restcontroller and Spring Data.


The jQuery datatable from http://datatables.net/ is a very nice fully functional javascript datatable with pagination, searching, sorting, etc. There are several customizations that make it very nice but also a little complicated to get setup depending on what you want. I wanted to have a "rest" endpoint provided by Spring MVC produce the results for the datatable.

The spring controller allowed me to pass in a "Pageable" org.springframework.data.domain.Pageble;. Which can be passed directly to the Spring Data repository without extracting the data and packaging it again for a query. Notice in the UserRepository.java there is no "userRespository.findAll(pageable);" as needed by line 46 in the UserController.java this is provided by PagingAndSortingRepository in SpringData. The "Page<User>" object gets directly returned in the response again eliminating an annoying unpackaging / repackaging scenario. The request URL looks very nasty. Because of all the fields the datatable adds on. I decided not to try and fight that.

http://localhost:8080/user/datatable.jquery?draw=2&columns%5B0%5D%5Bdata%5D=id&columns%5B0%5D%5Bname%5D=&columns%5B0%5D%5Bsearchable%5D=true&columns%5B0%5D%5Borderable%5D=true&columns%5B0%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B0%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B1%5D%5Bdata%5D=firstName&columns%5B1%5D%5Bname%5D=&columns%5B1%5D%5Bsearchable%5D=true&columns%5B1%5D%5Borderable%5D=true&columns%5B1%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B1%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B2%5D%5Bdata%5D=lastName&columns%5B2%5D%5Bname%5D=&columns%5B2%5D%5Bsearchable%5D=true&columns%5B2%5D%5Borderable%5D=true&columns%5B2%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B2%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B3%5D%5Bdata%5D=email&columns%5B3%5D%5Bname%5D=&columns%5B3%5D%5Bsearchable%5D=true&columns%5B3%5D%5Borderable%5D=true&columns%5B3%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B3%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B4%5D%5Bdata%5D=creationDate&columns%5B4%5D%5Bname%5D=&columns%5B4%5D%5Bsearchable%5D=true&columns%5B4%5D%5Borderable%5D=true&columns%5B4%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B4%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B5%5D%5Bdata%5D=&columns%5B5%5D%5Bname%5D=&columns%5B5%5D%5Bsearchable%5D=true&columns%5B5%5D%5Borderable%5D=true&columns%5B5%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B5%5D%5Bsearch%5D%5Bregex%5D=false&order%5B0%5D%5Bcolumn%5D=0&order%5B0%5D%5Bdir%5D=asc&start=5&length=5&search%5Bvalue%5D=&search%5Bregex%5D=false&page=1&size=5&sort=id%2Casc&_=1425242466260
This can be simplified to: (and still produce the same result).
http://localhost:8080/user/datatable.jquery?draw=2&page=1&size=5
The page and size fields make up the Pageable model object. On lines 10 thru 12 I configured the datatable to pass page, size, and sort to the the MVC controller. On the way out of the @RestController I package the response the way the datatable expects it in fields named "data", "draw", "recordsTotal", and "recordsFiltered".

Lastly I wanted view, edit, and delete links in the right column. This would be ugly code configured inline with the datatable so on line 28 I add small reference tags "<a class='dt-edit'></a>" to be used later.



User.java (Entity Bean)

package com.jot.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.joda.time.DateTime;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;

/**
 * @author Paul Mefford
 * @since 9/1/14
 *
 */
@Data //lombok 
@Entity
@Table(name = "user")
@EqualsAndHashCode(exclude = {"addresses"})
public class User  implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "user_id")
    private Long id;

    @Column(name = "user_name")
    private String username;
    @Column(name = "password")
    private String password;
    @Column(name = "firstname")
    private String firstName;
    @Column(name = "lastname")
    private String lastName;
    @Column(name = "user_type")
    private String userType;
    @Column(name = "email")
    private String email;
    @Column(name = "phone1")
    private String phone1;
    @Column(name = "phone2")
    private String phone2;
    @Column(name = "status")
    private String status;
    @Column(name = "creation_date")
    @Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    private DateTime creationDate;
    @Column(name = "security_role")
    private Integer role;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "userId")
    private List<useraddress> addresses = Collections.EMPTY_LIST;

}


UserRepository.java (Spring Data)

package com.jot.repository;

import com.jot.model.Company;
import com.jot.model.User;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.List;

/**
 * @author Paul Mefford
 * @since 9/6/14
 */

@Repository
public interface UserRespository extends PagingAndSortingRepository {
    User findByUsername(String username);
}


UserController.java (Spring MVC RestController)

package com.jot.web;

import com.google.common.collect.Lists;
import com.jot.model.LinkedUsers;
import com.jot.model.User;
import com.jot.repository.LinkedUserRepository;
import com.jot.repository.UserRespository;
import com.jot.services.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.mail.MessagingException;
import javax.validation.Valid;
import java.security.Principal;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * @author Paul Mefford
 * @since 9/6/14
 */
@RequestMapping("/user")
@Controller
public class UserController {
    Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserRespository userRespository;

    @RequestMapping("datatable.jquery")
    public @ResponseBody Map
    datatable(Model model, Pageable pageable, @RequestParam("draw") Integer draw , @RequestParam(value = "search", defaultValue = "") String search){
        Map data = new HashMap<>();
        Page page = userRespository.findAll(pageable);
        data.put("data",page.getContent());
        data.put("draw",draw);
        data.put("recordsTotal",page.getTotalElements());
        data.put("recordsFiltered",page.getTotalElements());
        return data;
    }

}

HTML (Thymeleaf template engine)

<table class="display datatable" id="userTable" cellspacing="0" width="100%">
                <thead>
                <tr>
                    <th>Id</th>
                    <th>First Name</th>
                    <th>Last Name</th>
                    <th>Email</th>
                    <th>Created</th>
                    <th width="10%">&nbsp;</th>
                </tr>
                </thead>

                <tfoot>
                <tr>
                    <th>&nbsp;</th>
                    <th>&nbsp;</th>
                    <th>&nbsp;</th>
                    <th>&nbsp;</th>
                    <th>&nbsp;</th>
                    <th width="10%">
                        <a th:href="@{'/user?form'}">
                            <img src="../../../images/add.png" th:src="@{/images/add.png}" style="float: right"/>
                        </a>
                    </th>
                </tr>
                </tfoot>

                <tbody>


                </tbody>

            </table>


JavaScript

<script>
        //<![CDATA[

        $(document).ready(function() {
            $('#userTable').dataTable( {
                "ajax": {
                    "url":"user/datatable.jquery",
                    "data":function(d) {
                        var table = $('#userTable').DataTable()
                        d.page = (table != undefined)?table.page.info().page:0
                        d.size = (table != undefined)?table.page.info().length:5
                        d.sort = d.columns[d.order[0].column].data + ',' + d.order[0].dir
                    }
                },
                "searching":false,
                "processing": true,
                "serverSide": true,
                "lengthMenu": [[5, 10, 15,30,50,75,100], [5, 10, 15,30,50,75,100]],
                "columns": [
                    { "data": "id" },
                    { "data": "firstName" },
                    { "data": "lastName" },
                    { "data": "email" },
                    { "data": "creationDate" },
                    { "": "" }
                ],
                "columnDefs": [
                    { "data": null, "targets": -1, "defaultContent":"<h4><a class='dt-view'></a><a class='dt-edit'></a><a class='dt-delete'></a></h4>" }
                ],
                "pagingType": "full_numbers"

            } );

        } );

        //]]>
    </script>


Add Links / Icons to last Column


$("#userTable").on('draw.dt',function(){
            $(".dt-view").each(function(){
                $(this).addClass('text-success').append("<span class='glyphicon glyphicon-search' aria-hidden='true'></span>");
                $(this).on('click',function(){
                    var table = $('#userTable').DataTable();
                    var data = table.row( $(this).parents('tr') ).data();
                    window.location = 'user/'+data.id;
                });
            });

            $(".dt-edit").each(function(){
                $(this).addClass('text-default').append("<span class='glyphicon glyphicon-edit' aria-hidden='true'></span>");
                $(this).on('click',function(){
                    var table = $('#userTable').DataTable();
                    var data = table.row( $(this).parents('tr') ).data();
                    var path =  'user/'+data.id+'/update';
                    $("<form action='"+path+"'></form>").appendTo('body').submit();
                });
            });

            $(".dt-delete").each(function(){
                $(this).addClass('text-danger').append("<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>");
                $(this).on('click',function(){
                    var table = $('#userTable').DataTable();
                    var data = table.row( $(this).parents('tr') ).data();
                    var path =  'user/'+data.id+'/delete';
                    $("<form action='"+path+"'></form>").appendTo('body').submit();
                    $.simplyToast('danger', data.firstName+ ' has been deleted');
                });
            });
        });

Tuesday, June 8, 2010

Struts 2: Locale Specific Validation XML

Problem:

Using one form over multiple countries with fields possibly changing business rules and validation requirements per country.
The Struts 2 validation system is a very elegant way to validate form input on the client and server side. We wanted to use the validation components including the struts-tags but the struts validation system is very much a black box without much information about how to tear it apart. Our other requirements were that we had a dynamic form changing based on country. This didn't seem like a unique problem to me for those developing enterprise software this is a common situation. In fact the struts documentation goes into localization of labels (which didn't work for us because we use a database driven local system).

Solution:

1.
There is possibly a better solution but this is what I came up with that will work for our needs.  We decided to use the wild card syntax in our action name to allow us to append the country on the end of the URL.
<action class="com.myBiz.SomeAction" name="Save*">
             <result>success.jsp</result>
</action>

2. 
Once we have that in place then we can use the validation 'alias' to capture the correct XML definition for our rules.  It wasn't completely clear to me what the alias was but after some quick testing it correlates with the name attribute of the action.  So here is the name of my validation definition.
SomeAction-SaveUS-validation.xml
I can also use the main validation for a default where fields that don't have special rules per country can be defined.
SomeAction-validation.xml
Keep in mind however that all defined rules will be executed so if you were to require firstName in the SomeAction-validation.xml and in the SomeAction-SaveUS-validation.xml both required will be executed and you will have double error messages.

3.
The last part of this is telling the form what country it needs to submit too.

<form action="Save${country}" > </form> Something like that.

I don't think this is the most elegant solution and there will probably be gotchas along the way.  However, this solution fits our needs today and I have a deadline to meet.


Monday, May 10, 2010

Struts 2 Package Extended

When we first started using struts 2 we wanted to keep parts of the application clean by using separate packages for different parts of the app. However we found that we were copying interceptors all over the place because we wanted to use the same functionality in the separate packages. I know the struts gurus out there are saying "duh! extend the package". Well my biggest gripe with struts is the documentation says things like "You can extend the package" or they will give you a snipplet of xml without any context.

How to extend your package:


Our struts.xml looks like this now.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">



<struts>
<!--<constant name="struts.enable.DynamicMethodInvocation" value="false" />-->
<!--<constant name="struts.devMode" value="true" />-->

<include file="admin-config.xml" />
<include file="reports-config.xml" />


<package name="mybiz-struts-default" extends="struts-default" abstract="true">
<interceptors>
<!--  <!– SessionTimeout interceptor will redirect to SessionTimeout action if problem is detected –>-->
<interceptor name="SessionTimeout" class="com.mybiz.interceptors.CheckTimeout"/>
<!--<!– CookieRedirect interceptor will redirect to CheckCookies action if problem is detected –>-->
<interceptor name="CookieRedirect" class="com.mybiz.interceptors.CookieRedirect"/>
<interceptor-stack name="PrepareSessionStack">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="SessionTimeout"/>
<interceptor-ref name="CookieRedirect"/>
<interceptor-ref ... other interceptors .../>
</interceptor-stack>
</interceptors>
<global-results>
<result name="cookieRedirect">../ErrorUIMessages.jsp</result>
<result name="error">../ErrorUIMessages.jsp</result>
<result .... other results ....>
<result name="errorPage">../error/Error.jsp</result>
</global-results>
</package>
</struts>

There are a few parts that I would like to highlight.
  1. mybiz-struts-default
  2. CookieRedirect interceptor
  3. global-results
  4. Creating your own interceptor stack

mybiz-struts-default


This package extends the struts-default package. All of our other packages extends mybiz-struts-default. This way we have one definition that can be used by all our packages instead of copying and pasting the definition everywhere.
Here is an example of a package for one of the apps reports-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="mybiz" namespace="/mybiz" extends="mybiz-struts-default">
<action name="CheckCookies" class="com.mybiz.actions.core.CheckCookiesEnabled">
<result name="cookiedisabled">../ErrorUIMessages.jsp</result>
</action>
<action name="SessionTimeout" class="com.mybiz.actions.core.ActionTimeout">
<result>../ErrorUIMessages.jsp</result>
</action>
<action name="action1" class="com.mybiz.actions.core.Action1">
<interceptor-ref name="PrepareSessionStack"/>
<result>..action1.jsp</result>
</action>
<.... more actions ....>
</package>
</struts>

Now, I have gone and complained about code snipplets and then gone and given you code snipplets. Hopefully you can make sense of it.

CookieRedirect interceptor


Most modern applications don't function very well without cookies enabled. Tomcat uses the JSESSIONID cookie to keep track of the session. The documentation claims that if cookies are disabled tomcat switch to url-rewrite appending the session id on the end of every url. I have not been able to get this to work in tomcat 5.5. Especially adding a layer of apache and clustering servers it just gets really complicated.
So we decided that the user must really have cookies enabled. The only way to determine if cookies are enabled is to set a cookie and check for it on a subsequent request. I decide that the first request should have the JSESSIONID cookie set so I would redirect and check for the cookie. If the cookie is there I redirect back to the page they requested. If the cookie is not there they get sent to a page that explains why we need cookies and how to enable them.

global-results


Above I was explaining how I solved the cookies enabled issue. Well in an interceptor if the cookie redirect check had not happened I return "cookieRedirect" but the actual request has been redirected to the checkaction. Well this caused the stacktrace to print out an error unless every action had a result of "cookieRedirect" defined. I didn't want to make my team define this on every action and have to explain why all the time.
Global results solved this problem for me. We define a result in global-results section with value "cookieRedirect".

Creating your own interceptor stack


Creating your own interceptor stack can be very powerful. You can add default behavior like forced login on every request. However, remember to include default-stack in your interceptor stack. If you forget to do this much of the default behavior you expect to work doesn't work. Like setting the values of request parameters in your action pojo. This oversight could take an afternoon to figure out.

Validation Internationalization (Struts 2)

Struts 2 has a very powerful form validation system using the commons validation. If you are just getting your feet wet I recommend you read http://struts.apache.org/2.x/docs/guides.html.

After implementing the validation I had 2 issues. The first was internationalization of the error messages contained in the -validation.xml. The second was country specific validation for the same form.

Internationalization Error Messages


The struts validation has its own system for internationalization of error messages in properties files. As I mentioned on the opening page we are re-factoring our way into a struts 2 architecture. We already have a system wide internationalization system that is database driven and it would be too expensive to switch to a properties file based system. So the challenge is using the cool parts of struts without using the full thing. I created my -validation.xml in the same directory as my action class name MyAction-validation.xml. (note. your fields in -validation.xml must be defined in both the .html form file and in the action class to validate. That took almost half a day to figure out because I added new fields in the html and didn't get to adding them to the action.)
<validators>
<...........>
<field name="email">
<field-validator type="requiredstring">
<message>enroll.validator.email.req</message>
</field-validator>
<field-validator type="email">
<message>enroll.validator.isp.email.invalid</message>
</field-validator>
</field>

<..............>
</validators>

Solution:


Notice the message is not something like "Your email address is required". I used a tag 'enroll.validator.email.req' instead. The other part of the solution required me to extend the them xhtml and use an object in the session to find the actual message from the tag. In controlheader-core.ftl you will find <span class="errorMessage">${error?html}</span>. I changed that to <span
class="errorMessage">${Session.LANGXLAT.xlat(error?html)}</span>. The LANGXLAT is the name of the attribute in the session. It is a custom translator object that takes a tag and returns the text in the language the user is browsing in. (note: when I extended the theme it wasn't copying into my class path because I had the java compiler output going into /WEB-INF/classes but the theme files were not java and so I had to have a separate deployment step copy the template folder from my src to WEB-INF/classes. My preferred ide is JetBrains IntelliJ and it has some configuration issues I have had to deal with. I blame it on not be able to know everything now.)

<s:debug />


There is a bit of magic happening with the validation system. Its often tough to figure out whats going on or why your fields aren't getting validated. The tag from struts-tags <s:debug /> is very helpful. If you don't use any tags use that tag. Just include <%@ taglib prefix="s" uri="/struts-tags" %> at the top of your file and <s:debug /> at the bottom. It will print out a bunch of data that is in the stack and whats been validated or the error messages.

Interpolated Values


Struts validation has the ability to interpolate values into the error messages (ie.<message>bar must be between ${min} and ${max}, current value is ${bar}.</message>). I haven't had to deal with this yet in my solution but I am sure I would create some sort of syntax, comma separated, to capture the values from the <message> and then parse them out later when I put into my 'view'.

Recommended Books:
Apache
Struts 2 Web Application DevelopmentApache
Struts 2 Web Application Development: Design, Develop, Test, and Deploy
Your Web Applications Using the Struts 2 Framework
ISBN: 9781847193391
by Dave Newton
Packt Publishing Copyright Pact Publishing © 2009 (384 pages)
This book had some good information. Some things were still left to trial and error.