Thursday, February 10, 2011

Unable to read TLD "META-INF/tld/tiles-jsp.tld" from JAR file

I got this strange error saying:


org.apache.jasper.JasperException: Unable to read TLD "META-INF/tld/tiles-jsp.tld" from JAR file "file:/C:/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/MY_APP/WEB-INF/lib/tiles-jsp-2.0.4.jar": org.apache.jasper.JasperException: Failed to load or instantiate TagExtraInfo class: org.apache.tiles.jsp.taglib.UseAttributeTag$Tei
    at org.apache.jasper.compiler.DefaultErrorHandler.jspError(DefaultErrorHandler.java:51)
    at org.apache.jasper.compiler.ErrorDispatcher.dispatch(ErrorDispatcher.java:409)
    at org.apache.jasper.compiler.ErrorDispatcher.jspError(ErrorDispatcher.java:181)
    at org.apache.jasper.compiler.TagLibraryInfoImpl.(TagLibraryInfoImpl.java:182)
    at org.apache.jasper.compiler.Parser.parseTaglibDirective(Parser.java:386)
    .........



It turns out that I have both jsp-api.jar and tiles-jsp-2.*.jar in WEB-INF/lib together in my deployment folder.  They don't seem to like each other in the same place.  By removing jsp-api.jar, the problem is gone. 
Normally, jsp-api.jar should not be bundled in the war because all application server should have jsp-api.jar in their common library.  So this jar should not exists in application WEB-INF/lib folder.  The reason I had it in my project is because a bug currently exists in Eclipse Maven (m2e) plugin that includes jar files even they are set to provided scope.  In my project pom.xml I set the jsp-api to have the 'provided' scope.  However, the plugin still push the jar into my maven dependencies which are then port over to my tomcat deploy folder.  This is a known issue in Maven plugin.  If you have the same issue with me and has to use maven and tomcat together, you need to delete the file manually after server synchronization.  I wrote a simple batch file on my desktop as a short cut to delete the file.  Until the bug is fixed, this is the only workaround....

Eclipse version: Helio build 20100917-0705
M2E plugin version: 1.0.100.20110804-1717

Wednesday, February 9, 2011

Unit Testing JSP Custom Tag Using Spring

In order to unit test your JSP tag, you need to mock up ServletContext and PageContext.  This would be a difficult task without the help from 3rd party library.  Spring 2.5 provides a mock up liberary bundled in spring-test-2.5.x.jar.  This greatly speed up the construction of the test code.

Now I'm going to show an example on testing a custom tag using Spring test library:

Assuming we have a custom tag as below:

public class DataInquiryTag extends TagSupport {

 private String name;  // attribute parameter name
 private String size;  // HTML element size 
 
 @Override
 public int doStartTag() throws JspException {
  String msg = "Hello "+name+"";
  pageContext.getOut().write(msg);
  return super.doStartTag();
 } 
}

We test our tag using JUnit 4.
Before every test invocation, we need to mock ServletContext and PageContext. To do that we setup @Before method as below:

@Before
 public void setup(){
  // mock ServletContext
  mockServletContext = new MockServletContext();
  // mock PageContext
  mockPageContext = new MockPageContext(mockServletContext);  
  tag = new MyTag();
  tag.setPageContext(mockPageContext);  
 }
The @Before method will use spring mock library to mock ServletContext and PageContext. The is good enough for simple pojo tag that does not rely on any application context.
What if out tag is depending on application context? An example would be a class referenced in the test implements InitializingBean interface. The use of the class is to load properties and initialize values. This requires spring context to be loaded.
To load Spring Application Context, such as WebApplicationContext, we need to add following lines in @Before method:

String configLocations = "/WEB-INF/applicationContext.xml,/WEB-INF/applicationContext-mock.xml";
 mockServletContext.addInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, configLocations);
 ContextLoader loader = new ContextLoader();  
 loader.initWebApplicationContext(mockServletContext);
We first set the location of applicationContext file. Note that we add a applicationContext-mock.xml file to override beans defined in default context. This is handy because some beans may require external resource such as data source in order to be loaded correctly. We can override these beans to local pojo bean for the sake of unit testing.

Usually, application context is common to all tests so we only need to load spring context once before executing all tests. We can do so by adding these lines in @BeforeClass method. So our final setup looks like below:

@BeforeClass
 public static void init(){
  // mock ServletContext
  mockServletContext = new MockServletContext();
  //Add the following lines if your test depends on spring context to be loaded
  //For example, you have a referenced class that implements   
  org.springframework.beans.factory.InitializingBean interface
  String configLocations = "/WEB-INF/applicationContext.xml,/WEB-INF/applicationContext-mock.xml";
  mockServletContext.addInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, configLocations);
  ContextLoader loader = new ContextLoader();  
  loader.initWebApplicationContext(mockServletContext);
 }
 
 @Before
 public void setup(){
  // mock PageContext
  mockPageContext = new MockPageContext(mockServletContext);  
  tag = new MyTag();
  tag.setPageContext(mockPageContext);  
 }
The test setup as above to load application context at the beginning of all test run. Then every test will create its own PageContext to hold the request and response as well as a new tag instance.

After setting up the test environment, it's time to write our test:
@Test
 public void testDoStartTag() throws Exception{  
 try{
  tag.setName("John");
  tag.doStartTag();
  String output = ((MockHttpServletResponse)mockPageContext.getResponse()).getContentAsString(); 
  assert("Hello John
".equals(output)); }catch(JspException je){ assert(false); } }
The output string contains the actual http response (e.g. generated html code) of the tag when calling doStartTag method. So you can right specific assertion logic against the output of the tag.


The complete test class source is shown below:

public class TestDataInquiryTag {  
 @BeforeClass
 public static void init(){
  // mock ServletContext
  mockServletContext = new MockServletContext();
  //Add the following lines if your test depends on spring context to be loaded
  //For example, you have a referenced class that implements 
  //org.springframework.beans.factory.InitializingBean interface
  String configLocations = "/WEB-INF/applicationContext.xml,/WEB-INF/applicationContext-mock.xml";
  mockServletContext.addInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, configLocations);
  ContextLoader loader = new ContextLoader();  
  loader.initWebApplicationContext(mockServletContext);
 }
 
 @Before
 public void setup(){
  // mock PageContext
  mockPageContext = new MockPageContext(mockServletContext);  
  tag = new MyTag();
  tag.setPageContext(mockPageContext);  
 }

 @Test
 public void testDoStartTag() throws Exception{  
  try{
   tag.setName("John");
   tag.doStartTag();
   String output = ((MockHttpServletResponse)mockPageContext.getResponse()).getContentAsString(); 
   assert("Hello John
".equals(output)); }catch(JspException je){ assert(false); } } }