Wednesday, April 8, 2009

Struts 2, Apache Tiles 2 - View Preparer - Dynamic Menu Creation

Let us develop as page using Struts 2, Apache Tile and Spring. This page has the following tiles – Header, Menu, Body and Footer. Let us have our Menu which is dynamic in nature which gets all menu items from database. So, we will use View Preparer to prepare the Menu before rendering.

Our tile definition file (tiles.xml) would look like

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 2.0//EN"
"http://jakarta.apache.org/struts/dtds/tiles-config_2_0.dtd">

<tiles-definitions>
<definition name="test.homepage" template="/htdocs/layouts/homepage.jsp">
<put-attribute name="logo" value="/htdocs/tiles/logo.jsp" />
<put-attribute name="menu" value="test.menu" />
<put-attribute name="body" value="/htdocs/tiles/body.jsp" />
<put-attribute name="footer" value="/htdocs/tiles/footer.jsp" />
</definition>

<definition name="test.menu" preparer="menuPreparer" template="/htdocs/tiles/menu.jsp" />

</tiles-definitions>

I have associated a preparer “menuPreparer’ to the tile definition for the menu. “menuPreparer” is the bean defined in applicationContext.xml below. Our bean definition file (applicationContext.xml) looks like

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">

<context:annotation-config />

<bean id="menuService" class="test.MenuService"
/>

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/tiles.xml</value>
</list>
</property>
<property name="preparerFactoryClass" value="org.springframework.web.servlet.view.tiles2.SpringBeanPreparerFactory"/>
</bean>

<bean id="menuPreparer" class="test.MenuViewPreparer"
/>
</beans>

Here, we have defined a bean test.MenuViewPreparer whose id “menuPreparer” is defined in tiles.xm above.

test.MenuViewPreparer implements ViewPreparer interface whose execute method is called before tile is rendered. In this execute method we will get the list of all the menus to be displayed. We will use the service test.MenuService to get the list of all menu items. This service can get all the menus from database. This service (bean) is defined in the applicationContext above.

Now we have to inject menuService to test.MenuViewPreparer. So, how do I do this?

I will use annotations to configure Spring’s dependency injection. So, I have <context:annotation-config /> in my applicationContext.xml.

This is my test.MenuViewPreparer

package test;

import java.util.List;

import org.apache.tiles.Attribute;
import org.apache.tiles.AttributeContext;
import org.apache.tiles.context.TilesRequestContext;
import org.apache.tiles.preparer.PreparerException;
import org.apache.tiles.preparer.ViewPreparer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MenuViewPreparer implements ViewPreparer {
@Autowired
private IMenuService menuService;

private List<Menu> menuList;

public IMenuService getMenuService() {
return menuService;
}

public void setMenuService(IMenuService menuService) {
this.menuService = menuService;
}

public List<Menu> getMenuList() {
return menuList;
}

public void setMenuList(List<Menu> menuList) {
this.menuList = menuList;
}

public void execute(TilesRequestContext tilesContext,
AttributeContext attributeContext) throws PreparerException {
menuList = menuService.getMenuList();
attributeContext.putAttribute("menuItems", new Attribute(menuList));
System.out.println(menuList);

}

}

Here I have used @Service and @Autowired. @Autowired is used to autowire the dependency of the MenuViewPreparer on the IMenuService. Here is the IMenuService interface.

package test;

import java.util.List;

public interface IMenuService {

List<Menu> getMenuList();

}

Here is test.Menu

package test;

public class Menu {
private String menuName;

private String menuURL;

public Menu(String menuName, String menuURL) {
super();
this.menuName = menuName;
this.menuURL = menuURL;
}

public String getMenuName() {
return menuName;
}

public void setMenuName(String menuName) {
this.menuName = menuName;
}

public String getMenuURL() {
return menuURL;
}

public void setMenuURL(String menuURL) {
this.menuURL = menuURL;
}
}

At last let us have a look at homepage.jsp and menu.jsp. This is my homepage.jsp

<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<html>
<head>
<title>Test</title>
</head>

<body>
<div id="main">
<tiles:insertAttribute name="logo"/>
<tiles:insertAttribute name="menu"/>
<tiles:insertAttribute name="body"/>
<tiles:insertAttribute name="footer"/>
</div>
</body>
</html>

menu.jsp looks like –

<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c" %>

<tiles:importAttribute name="menuItems" />
<c:forEach var="item" items="${menuItems}">
<a href="${item.menuURL}"> ${item.menuName}</a>

</c:forEach>

<br><br>

Here I have used JSTL to display the menu items. Test.MenuViewPreparer’s execute method puts the attribute menuItems in attributeContext. So, on JSP I get this attribute and display the menu (name and URL) on the page.

Libraries used –

Apart from Struts2-core, xwork, tiles-api, tiles-core these jars are required

spring.jar (Spring 2.5 and above)
JSTL jars - standard.jar, jstl.jar

Note – You do not have to initialize “StrutsTilesListener” in your web.xm. As we are using Spring to load the tile definitions.

Some interesting Apache Rewrite rules

Example 1
I have a directory having installers for different languages. User can download installer for any language. If user requests an installer for a language that is not supported, then the “en” installer has to be provided.

These installers are stored as -

<DOCUMENT_ROOT>/installers/<TWO_CHARS_LOCALE>/installer.exe like,
<DOCUMENT_ROOT>/installers/en/installer.exe,
<DOCUMENT_ROOT>/installers/de/installer.exe etc.

User can request an installer using the link http://www.mysite.com/installers/en/installer.exe

How do we achieve this language fallback – if an installer for a language is not available, provide English installer?

One way of achieving it is to write a 404 handler.

But how about writing a simple rewrite rule?

RewriteEngine On
RewriteLog "logs/rewrite.log"
RewriteLogLevel 9
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f [NC]
RewriteRule (^/installers)/../(installer.exe)$ $1/en/$2 [L]

More examples to follow soon ..

Tuesday, April 7, 2009

Spring Quartz

Let us now integrate Spring Quartz with the previous example. Let us check if there are any new match detail available every 1 hour. If we get new match detail; add it to the previously stored match details, create the new JSON feed and publish it.

Integrating Quartz Scheduler with Spring is very easy. You just have to add required beans into your bean definition file (applicationContext.xml). So, in our case it would look like –

< beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" >

< ! - -
This will call generateFeed method on feedGenerator bean defined below. To make jobs resulting from MethodInvokingJobDetailFactoryBean as non-concurrent, I have set the concurrent flag to false.
- - >

< bean id="jsonFeedJob" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
< property name="targetObject" ref="feedGenerator" />
< property name="targetMethod" value="generateFeed" />
< property name="concurrent" value="false" />
< /bean >

< ! - -
matchService bean is defined that has business logic to get the recent match detail. May be getting it from database or some other third party feed.
- - >
< bean id="matchService" class="test.SomeMatchService" / >

< ! - -
feedGenerator has the logic of generating JSON feed and has the method generateFeed(). SomeMatchService is injected into it
- - >
< bean id="feedGenerator" class="test.FeedGenerator"
p:matchService -ref="matchService"
/ >


< ! - -
Creates SimpleTriggerBean with start delay of 50 second and that repeats every 1 hour
- - >
< bean id="jsonFeedTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean" >
< property name="jobDetail" ref="jsonFeedJob" / >
< property name="startDelay" value="50000" / >
< property name="repeatInterval" value="3600000" / >
< / bean >

< ! - -
Finally adding the SimpleTriggerBean to SchedulerFactoryBean
- - >
< bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
< property name="triggers" >
< list >
< ref bean="jsonFeedTrigger" / >
< /list >
< /property >
< /bean >
< /beans >

Now add the following class to start your job

package test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class JsonJobHandler {
public static void main (String [] args) {
String[] paths = { "file:applicationContext.xml" };
ApplicationContext ctx = new ClassPathXmlApplicationContext(paths);
}
}

Instead of generating feed every 1 hour, if we want to generate feed every morning at 5:30 AM we can use CronTriggerBean instead of SimpleTriggerBean

<bean id="jsonFeedTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="jsonFeedJob" />
<property name="cronExpression" value="0 30 5 * * ?" />
</bean>

Isn’t So Easy?

JSON Feed using XStream

Let us write a sample code to generate JSON feed for some matches that can be shared across websites. JSON format looks like as mentioned below – It is an array of several matches played. We have to fetch the match detail (recent match detail) every after ‘n’ minute and create an array having all old match details and the recent match detail. Match detail has match id, match play time, prize amount given and players.

{
"matchDetails": [
{
"matchId": 101,
"time": 937858019,
"prizeAmount": 10000,
"players": [
"John",
"Jean",
"Mike"
]
},
{
"matchId": 102,
"time": 937858019,
"prizeAmount": 12000,
"players": [
"Vivian",
"Angela",
"Andrew"
]
}
]
}


I have used Spring Quartz for scheduling job and XStream to convert java object into JSON.

XStream is very powerful library that converts Java Object to XML / JSON and back again.

So, let us start with writing java class MatchDetail that holds match details.

package test;

import java.util.List;

public class MatchDetail {
private int matchId;

private long time;

private int prizeAmount;

private List players;

public int getMatchId() {
return matchId;
}

public void setMatchId(int matchId) {
this.matchId = matchId;
}

public long getTime() {
return time;
}

public void setTime(long time) {
this.time = time;
}

public int getPrizeAmount() {
return prizeAmount;
}

public void setPrizeAmount(int prizeAmount) {
this.prizeAmount = prizeAmount;
}

public List getPlayers() {
return players;
}

public void setPlayers(List players) {
this.players = players;
}

}

This class generates the JSON Feed –

package test;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
import com.thoughtworks.xstream.io.json.JsonWriter;

public class FeedGenerator {
IMatchService matchService;

public IMatchService getMatchService() {
return matchService;
}

public void setMatchService(IMatchService matchService) {
this.matchService = matchService;
}

public void generateFeed() throws Exception {
// Get the recent match details
MatchDetail detail = getRecentMatchDetail();

// Get the stored feed
List details = getMatchDetails();

// Add the current match detail to the list
details.add(detail);

// Save it back to the disk
storeMatchDetails(details);

// I have to create JSON – format as shown above, so I write the
// following to store match details as JSON.
saveMatchDetailsAsJSON(details);

}

private MatchDetail getRecentMatchDetail() {
return matchService.getRecentMatchDetail();
}

private List getMatchDetails() throws Exception {
XStream xstream = new XStream();

File file = new File("feed.xml");
if (!file.exists()) {
return null;
}

FileReader fr = new FileReader(file);
ObjectInputStream in = xstream.createObjectInputStream(fr);
List wmds = (ArrayList) in.readObject();
in.close();
fr.close();
return wmds;
}

private void storeMatchDetails(List whds) throws Exception {
XStream xstream = new XStream();

File file = new File("feed.xml");
FileWriter fw = new FileWriter(file);
ObjectOutputStream out = xstream.createObjectOutputStream(fw);

out.writeObject(whds);
out.close();
fw.close();
}

private void saveMatchDetailsAsJSON(List whds)
throws Exception {
XStream xstream = new XStream(new JsonHierarchicalStreamDriver() {
public HierarchicalStreamWriter createWriter(Writer writer) {
return new JsonWriter(writer, JsonWriter.DROP_ROOT_MODE);
}
});

xstream.setMode(XStream.NO_REFERENCES);
xstream.alias("matchDetails", List.class);

File file = new File("matchDetails.js");
FileWriter fw = new FileWriter(file);
ObjectOutputStream out = xstream.createObjectOutputStream(fw);

out.writeObject(whds);
out.close();
fw.close();
}

}

I have to use intermediate feed.xml to store match details. I could have used matchDetails.js but after using “DROP_ROOT_MODE”, alias etc (as I need the JSON format as mentioned above) I was not able to convert JSON back to java object.

Libraries Used –

xstream-1.3.1.jar
xpp3_min-1.1.4c.jar
jettison-1.0.1.jar

Next we will discuss Spring Quartz