brush clojure

Tuesday, March 16, 2010

How to de-FRAMESET a servlet application using Apache Tiles 2

The problem


There's is a good old application from the days when struts still had to be invented and framesets weren't considered evil. The problems associated with a FRAMESET are another topic, the fact is the application owner wants to get rid of the FRAMSET and asked me to look for a solution that would be unobtrusive and easy to implement, understand and maintain.

Current Situation


The application uses no web-app framework, each Use Case is implemented in a separate servlet, all of them neatly mapped in the web.xml. The FRAMESET setup is classic: apart from the static elements, the navigation happens in a 'body' and a 'menu' frame. The normal flow is to select a command from the menu frame, e.g. Place Order results in (<a href="/placeorder" target="'body"/>), then submit a few forms in the body frame (<form action="/placeorder" method="POST">...</form>). Without frameset, when a HTTP request for /placeorder reaches the application, we want the servlet response decorated with the other fragments: menu, header and the rest. The differences are essential: there is just one request/response and all page fragments have to share the same HEAD, CSS etc.

Introducing Apache Tiles 2


The good news is that version 2.x of Apache Tiles, which has started as struts extension, is now framework-independent. It offers support for JSP, Freemarker, and can be used directly from a servlet or filter. First of all, a template is needed to replace the frameset, e.g. main.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles"%>
<html>
<head>
 <link href="/online/theme/online.css" rel="stylesheet" type="text/css">
    <script language="javascript" src="/online/js/algemeen.js"></script>
</head>
<body>
<table border="1" cellspacing="0" cellpadding="0"
 style="width: 100%; height: 100%">
 <tr>
  <td colspan="2"><tiles:insertAttribute name="top" /></td>
 </tr>
 <tr>
  <td><tiles:insertAttribute name="menu" /></td>
  <td><tiles:insertAttribute name="body" /></td>
 </tr>
 <tr>
  <td colspan="2"><tiles:insertAttribute name="bottom" /></td>
 </tr>
</table>
</body>
</html>
Based on that template a tile definition can render a decorated view of any servlet output (e.g. /placeorder). A definition per servlet is needed in tiles.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 2.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_2_0.dtd">

<tiles-definitions>
  
 <definition name="placeorder.tile" template="/layout/main.jsp">
  <put-attribute name="top" value="/algemeen/onlinetop.html" />
  <put-attribute name="menu" value="/besturing/menu.jsp" />
  <put-attribute name="body" value="/placeorder" />
  <put-attribute name="bottom" value="/algemeen/onlinebottom.html" />
 </definition>
</tiles-definitions>

This definition still has to be rendered, e.g. from another JSP, let's call it placeholder_tiled.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>

<tiles:insertDefinition name="placeorder.tile" />

A separate tile definition and a JSP page for each servlet would be too much code repetition. Let's see how to the output of any servlet.

A View decorating servlet


I like the idea of having separate URLs for the servlet output and for a composite view of the same output. Thanks to Tiles 2 API, one servlet could provide decorated views of the output of all servlets. In tiles.xml, I added a tile definition with all attributes except body defined:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 2.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_2_0.dtd">

<tiles-definitions>
 <definition name="main.tile" template="/layout/main.jsp">
  <put-attribute name="top" value="/algemeen/onlinetop.html" />
  <put-attribute name="menu" value="/besturing/menu.jsp" />
  <!-- name="body"  not defined -->
  <put-attribute name="bottom" value="/algemeen/onlinebottom.html" />
 </definition>
 
</tiles-definitions>


Let's define a servlet that will fill in the missing body attribute for each URL ending with .tiled:

<servlet>
 <display-name>TilesServlet</display-name>
 <servlet-name>TilesServlet</servlet-name>
 <servlet-class>com.assenkolov.common.web.TilesServlet</servlet-class>
 <init-param>
 <param-name>template.name</param-name><param-value>main.tile</param-value></init-param>
 <init-param>
 <param-name>attribute.name</param-name><param-value>body</param-value></init-param>
</servlet>

<servlet-mapping>
 <servlet-name>TilesServlet</servlet-name>
 <url-pattern>*.tiled</url-pattern>
</servlet-mapping>

Finally, the servlet itself:
public class TilesServlet extends HttpServlet {
 private String templateName;
 private String attributeName;

 @Override
 public void init() throws ServletException {
  super.init();
  templateName = getInitParameter("template.name");
  attributeName = getInitParameter("attribute.name");
 }

 @Override
 public void service(HttpServletRequest request, HttpServletResponse response) 
  throws  ServletException, IOException {
   ServletContext servletContext = request.getSession().getServletContext();

   TilesContainer container = ServletUtil.getContainer(servletContext);
   AttributeContext attributeContext = container.startContext(request, response);

   // Remove the ".tiled" suffix from the request path
   String requestPath = request.getServletPath().replace(".tiled", "");
   Attribute attr = new Attribute(requestPath);

   Map attrs = new HashMap();
   attrs.put(attributeName, attr);
   attributeContext.addAll(attrs);

   container.render(templateName, request, response);
   container.endContext(request, response);
  }
}

This servlet always renders the same tiles definition, providing an appropriate body. No separate tiles definition and JSP for each view.

What's next?

All the code above only provides an easy template-decorated view for each servlet output. There is sill a lot of work that has to be done manually:
- move all separate styles, scripts etc. from the separate pages to the template
- requestDispacher.forward in a servlet closes the output stream while it is still needed for the tiles template; all forwards must become includes;
- all navigation javascript referring to top.menu ot top.body has to be refatored manually.

No comments:

Post a Comment