[ Team LiB ] |
4.3 Advanced ViewsUntil now, we have treated the view as a black box, assuming that a single JSP page will convert our model data into HTML to send to the user. In reality, this is a tall order. Large JSP pages with lots of embedded code are just as unwieldy as a large front controller. Like the controller, we would like to break the view into a generic framework with the specialized pieces separated out. The challenge is to break up the view within the restricted programming model of JSP. Remember that one of our goals was to minimize embedding code in our JSP pages, since it blurs the line between the view and the controller. Thus, we will avoid the kinds of classes and interfaces we used to solve the same problem in the controller. Fortunately, JSP gives us a different set of tools to work with: JSP directives and custom tags. We will use both of these extensively in the next two patterns to help separate the view into reusable components. 4.3.1 The View Helper PatternOne mechanism for reducing specialization in views is a view helper. A view helper acts as an intermediary between the model and the view. It reads specific business data and translates it, sometimes directly into HTML, and sometimes into an intermediate data model. Instead of the view containing specialized code to deal with a particular model, the view includes more generic calls to the helper. Figure 4-4 shows how a view uses helpers. Figure 4-4. A view using helpersView helpers increase reusability in two ways: by reducing the amount of specialized code in a view, helpers make views more reusable; and, since a helper encapsulates a specific kind of interaction with the model, helpers can be reused themselves. 4.3.2 Implementing a View HelperWhen you think about view helpers in JSP, custom tags should immediately pop to mind. Conceptually, custom tags fit the bill—they adapt Java objects into JSP markup. Moving code embedded in JSP into custom tag classes reduces coupling, since the tag defines a clear interface independent of the underlying objects. And since tags are grouped into libraries by function, they are inherently quite reusable themselves. While it is easy to think of all custom tags (or even all tags) as view helpers, they are not the same thing. A view helper is a tag, or set of tags, that translates model data into a convenient form for the view. A view helper may read business data in many forms, including JavaBeans, Enterprise JavaBeans, direct database access, or access to remote web services. For our example, let's look at the last of these: accessing a remote web service. Really Simple Syndication (RSS)[5] is an XML format that is the de facto standard for web sites to exchange headline information. RSS files are generally available via public HTTP from most news sites, at the very least. Example 4-6 shows a slightly simplified RSS file for a made-up news page, PatternsNews. (For simplicity, we have stuck to the 0.91 version of RSS. The current version, 2.0, is far more complicated.)
Example 4-6. PatternsNews.xml<?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN" "http://www.scripting.com/dtd/rss-0_91.dtd"> <rss version="0.91"> <channel> <title>Patterns news</title> <link>http://www.patternsnews.com</link> <item> <title>Local pattern-creators honored</title> <link>http://www.patternsnews.com/stories?id=0001</link> </item> <item> <title>Patterns solve business problem</title> <link>http://www.patternsnews.com/stories?id=0002</link> </item> <item> <title>New patterns discovered!</title> <link>http://www.patternsnews.com/stories?id=0003</link> </item> </channel> </rss> We would like to read RSS files and include their headlines on our own web site. In order to do this, we will build a view helper that makes the RSS data available as a JSP custom tag. Figure 4-5 shows the pieces we will build. Figure 4-5. A view helper for parsing RSS4.3.2.1 Parsing the RSSFor starters, we need to parse the RSS into a Java format that we can use. To do this, we create an RSSInfo class that parses RSS files and stores them as Java objects. We won't go into the specifics of how the parsing works, since the innards look a lot like the DOM-based XML parser we built for the last example. Example 4-7 shows the interface to the RSSInfo class. Example 4-7. The RSSInfo interfacepublic interface RSSInfo { // channel information public String getChannelTitle( ); public String getChannelLink( ); // item information public String getTitleAt(int index); public String getLinkAt(int index); public int getItemCount( ); // parse the RSS file at the given URL public void parse(String url) throws Exception; } The RSSInfo object represents an intermediate data structure: it is not exactly the underlying data, but also not exactly what we are using in the application. Building an intermediate data model may seem inefficient, but it can help significantly in reusability. Since the RSSInfo class is independent of any type of display mechanism, it can be used in many contexts: JSP custom tags, servlets, web services, etc. If the parsing was implemented directly as custom JSP tags, the logic would have to be duplicated. 4.3.2.2 Using the RSS: Custom tagsNow that we have a generic method for parsing RSS, we would like to use it in a JSP environment. We could store the RSSInfo object in the session and access it directly using JSP directives. While this option is quite flexible, it embeds a fair amount of logic for iteration and such in the JSP.[6] This logic would have to be rewritten for each page that used the RSS parser. As another option, we could create a custom tag that read the RSS and returned a preformatted table. This method has the advantage of being easy, since only one line of JSP is needed to include all the headlines. Unfortunately, it would mean including our page styles in the custom tag, and we would be unable to reuse the tag on a different page.
The best solution in this case is a hybrid: we will design custom tags to parse and iterate through the RSS data, and expose JSP scripting variables with the relevant values. This solution allows us the flexibility to format the output any way we want, while performing the heavy lifting in the custom tag logic. To format the RSS data as a table, for example, we would like to have our JSP look something like Example 4-8. Example 4-8. JSP custom tags in action<%@page contentType="text/html"%> <%@ taglib prefix="rss" uri="/ReadRSS" %> <html> <head><title>RSS Results</title></head> <body> <rss:RSSChannel URL="http://www.patternsnews.com/patternsnews.xml"> <b><a href="<%= channelLink %>"><%= channelName %></a></b> <br> <table> <rss:RSSItems> <tr><td><a href="<%= itemLink %>"><%= itemName %></a></td></tr> </rss:RSSItems> </table> </rss:RSSChannel> </body> </html> Here, our view helper consists of two custom tags. The first, RSSChannel, takes a URL corresponding to an RSS file and downloads and parses that file using the RSSInfo class. It exposes the channel title and channel link information in two scripting variables: channelTitle and channelLink. The second tag, RSSItems, performs a similar task, repeating its body for each item in turn and exposing the itemTitle and itemLink variables. The helper (the tags) is therefore responsible for creating and interpreting the intermediate data model (the RSSInfo). The RSSChannel tag source is shown in Example 4-9. Example 4-9. RSSChannelTag.javaimport javax.servlet.*;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
public class RSSChannelTag extends BodyTagSupport {
// scripting variable names
private static final String NAME_ATTR = "channelName";
private static final String LINK_ATTR = "channelLink";
// the input parameter
private String url;
// the RSS parser
private RSSInfo rssInfo;
public RSSChannelTag( ) {
rssInfo = new RSSInfoImpl( );
}
// called with the URL parameter from the tag
public void setURL(String url) {
this.url = url;
}
// used by the RSSItemsTag
protected RSSInfo getRSSInfo( ) {
return rssInfo;
}
// parse the RSS and set up the scripting variables
public int doStartTag( ) throws JspException {
try {
rssInfo.parse(url);
pageContext.setAttribute(NAME_ATTR, rssInfo.getChannelTitle( ));
pageContext.setAttribute(LINK_ATTR, rssInfo.getChannelLink( ));
} catch (Exception ex) {
throw new JspException("Unable to parse " + url, ex);
}
return Tag.EVAL_BODY_INCLUDE;
}
}
The RSSItems tag is slightly more complicated, since it implements the IterationTag interface. It loops through the item data, setting the itemTitle and itemLink variables with every pass. RSSItems also uses the findAncestorWithClass( ) method to locate the RSSInfo object that was stored by the parent. The RSSItemsTag source is shown in Example 4-10. Example 4-10. RSSItemsTag.javaimport javax.servlet.*;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
public class RSSItemsTag extends BodyTagSupport implements IterationTag {
// the names of the scripting variables
private static final String NAME_ATTR = "itemName";
private static final String LINK_ATTR = "itemLink";
// keep track of looping
private int counter;
// the stored RSS data, obtained from enclosing tag
private RSSInfo rssInfo;
public RSSItemsTag( ) {
super( );
counter = 0;
}
// find the RSSInfo from the enclosing tag and set the
// initial values of the scripting variables
public int doStartTag( ) throws JspException {
if (rssInfo == null) {
RSSChannelTag rct =
(RSSChannelTag)findAncestorWithClass(this, RSSChannelTag.class);
rssInfo = rct.getRSSInfo( );
}
pageContext.setAttribute(NAME_ATTR, rssInfo.getTitleAt(counter));
pageContext.setAttribute(LINK_ATTR, rssInfo.getLinkAt(counter));
return Tag.EVAL_BODY_INCLUDE;
}
// after each pass, increment the counter if there are still items left
// refresh the scripting variables at each pass
public int doAfterBody( ) throws JspException {
if (++counter >= rssInfo.getItemCount( )) {
return IterationTag.SKIP_BODY;
} else {
pageContext.setAttribute(NAME_ATTR, rssInfo.getTitleAt(counter));
pageContext.setAttribute(LINK_ATTR, rssInfo.getLinkAt(counter));
return IterationTag.EVAL_BODY_AGAIN;
}
}
}
With the custom tags, we have built a reusable view helper. The helper encapsulates a general mechanism for RSS parsing in an intermediate class, RSSInfo, and interprets the data using JSP custom tags. We could add other helpers, such as those to use the RSSInfo object in a web service. View helpers add a slight overhead to request processing. But the benefits, in terms of extensibility and reuse, tend to far outweigh these costs. By encapsulating common functionality, view helpers provide a simple way to organize otherwise complicated data access. We see an analogous pattern from the business object perspective when we look at the Session Façade pattern in Chapter 9. 4.3.3 Composite ViewViews tend to have a lot of repeated elements. This isn't a bad thing—a good user interface requires consistency. If the general presentation or the navigation were different from screen to screen within an application, the user would be very confused indeed. Think about the structure of web sites. Many sites follow a basic pattern, like the one shown in Figure 4-6: the content of the page is surrounded by a header on the top (usually with ads), a navigation area on the left, and a footer on the bottom. This consistent layout makes navigation within a site easy, and allows people unfamiliar with a new site to understand it quickly. Figure 4-6. A generic web site designIdeally, we would like the page's code to reflect this high-level organization. We should be able to specify the structure of the page in one generic template, and then vary the content on a per-application and per-page basis. Further, we should be able to apply this concept recursively, allowing each of the areas in our page to separate themselves into template and content. The Composite View pattern supports both of these requirements. 4.3.3.1 The Composite View patternThe Composite View pattern is based on the GoF composite pattern. The idea is fairly simple: treat the objects as a tree structure, in which parents expose the same interface as their children. When this concept is applied to a view, it means thinking of each page as composed of a number of elements. Each of the elements may be a leaf, which displays an actual widget to the screen. An element may also be a container, which contains multiple elements as children. The children of a container may be leaves, or other containers, resulting in the tree-like structure. Figure 4-7 shows the participants in a Composite View. Figure 4-7. Classes in the Composite View patternThe Composite View pattern is a staple of most graphical systems. In Java's Swing, for example, the user interface is built as a set of panels. Each panel contains any number of components, as well as other panels. View represents a common interface implemented by both leaves and containers. There are varying opinions on how rich this interface needs to be. Obviously, it must contain methods like draw( ), which are required by both leaves and containers. Methods that are specific to containers, like adding and removing children, could go either way. We could treat a leaf as a container with no children, thus allowing containers to export exactly the same interface as the leaves they contain (like in the Decorator pattern). Or we could put the container-specific interfaces only in the composite classes, saving overhead. In a system like Java, where runtime type-checking is relatively cheap, the container methods are usually restricted to the container, but either choice is valid. The LeafView and CompositeView objects implement the View interface. Leaves always display directly to the output. A composite must coordinate the display of all its subcomponents, as well as any rendering it might do itself. In some systems, the ordering of subcomponents within a composite is not important, but in displays it usually is. In a graphical application, there's frequently some concept of "layout"—a richer syntax for how components are arranged within a container. The layout might include the sizes and shapes of various components, for example. To be truly flexible, our solution needs to abstract layout within a container from the components themselves, a feat accomplished with a template. A template allows us to specify the high-level layout of a page, based on a generic set of components. The layout can be reused on different sets of components, allowing the view to vary dynamically. By modifying the template, we can make changes to the global layout in one central place. And templates may themselves contain other templates and components. In the same way that the Service to Worker pattern divided the controller into reusable dispatchers and actions, the Composite View pattern divides the view into reusable components. These components contain clear interfaces, so they can be shared, substituted, and extended at will. 4.3.3.2 Implementing composite viewsThe view tier in a web application has built-in support for a basic level of composition. Both JSPs and Servlets are used as views, but they can also be containers. The JSP include directive and the matching RequestDispatcher.include( ) method for servlets allow for the embedding of any other JSP page, servlet, or even static web page. While the servlet include mechanism is quite flexible, the JSP include directive is not. Including a page requires embedding its URL into the parent page, creating a tight coupling between the parent and the child. We want to avoid this coupling, so that we can reuse the parent page on different sets of children. In this example, we build the JSP custom tags to support the notion of containers. One tag defines a container and one defines an element in a container. The container tag determines the name of the container and and the container maps that label at the back end to support the include tag. The include tag works like the normal JSP include tag, except that instead of specifying a URL, we specify a label, and the container maps that label to different URLs depending on the page being viewed. An include tag is always nested within a container tag. Example 4-11 shows how a container might be used to create a layout similar to that in Figure 4-6. Example 4-11. MainTemplate.jsp<%@page contentType="text/html"%> <%@ taglib uri="/container" prefix="cv" %> <cv:container name="main"> <html> <head><title><cv:include label="title" /></title></head> <body> <table width="100%" height="100%"> <tr><td><cv:include label="header" /></td></tr> <tr><td><cv:include label="body" /></td></tr> <tr><td><cv:include label="footer" /></td></tr> </table> </body> </html> </cv:container> We populate the template with an XML file. It maps the "title," "header," "body," and "footer" labels to different URLs, depending on what screen is being shown. To do this, we will create an XML document similar to the one we used for workflows. Example 4-12 shows how this format could be used for the "main" container. Example 4-12. Views.xml<?xml version="1.0" encoding="UTF-8"?> <views> <view name="page1" template="MainTemplate.jsp"> <container name="main"> <include label="header" url="Header.html" /> <include label="footer" url="Footer.html" /> <include label="body" url="page1.html" /> </container> </view> <view name="page2" template="MainTemplate.jsp"> <container name="main"> <include label="header" url="Header.html" /> <include label="footer" url="Footer.html" /> <include label="body" url="page2.html" /> </container> </view> </views> We now have a view element for each page in the application that specifies the template to be used. Each view consists of one or more container elements. The name attribute of a container matches the name attribute specified in the container tag in the JSP. While this might seem overly complicated, it has the advantage of supporting nesting. As long as container names are unique within a given page, as many containers as desired can be put into a single view.
In this example, each page lists MainTemplate.jsp as its view, but specifies different values for the "body" include. When the cv:include tag is encountered in the JSP above, it substitutes the appropriate URL from the XML file. 4.3.3.3 Reusing the front controller and dispatcherTo support this at the backend, we will extend our earlier Service to Worker example. We will use the same front controller class but create a new dispatcher, the CompositeDispatcher. The composite dispatcher parses the XML file and stores view information in a View object. Example 4-13 shows the View object's interface. Example 4-13. The View interfacepublic interface View { // get the url for a given container and label public String getURL(String container, String label); // check if there is a record for the given container public boolean hasContainer(String container); // get the template this view uses public String getTemplate( ); } Once again, we won't go into the messy details of storing View information or parsing XML. Once the XML is parsed, the CompositeDispatcher stores each view indexed by name in a HashMap called "views." When a request comes in, it expects the page name in a request attribute. The dispatcher simply looks up the appropriate view object and stores it in the request so that the container tags can use it. The getNextPage( ) method of this dispatcher looks like this: public String getNextPage(HttpServletRequest req, ServletContext context) { String page = (String)req.getParameter(PAGE_ATTR); View v = (View)views.get(page); if (v == null) { v = (View)views.get(DEFAULT_VIEW_NAME); } req.setAttribute(VIEW_ATTR, v); return (v.getTemplate( )); } 4.3.3.4 Building the custom tagsOnce again, JSP custom tags prove to be a powerful and efficient mechanism for extending the view. In this case, using tags in combination with a dispatcher simplifies them even further. Our first tag, the ContainerTag, simply looks up the appropriate container in the View object provided by the dispatcher. If the container exists, it is stored for use by the ContainerIncludeTag. If it does not, the entire contents of the container are skipped. Example 4-14 shows the ContainerTag source. Example 4-14. ContainerTag.javaimport javax.servlet.jsp.tagext.*; import javax.servlet.jsp.*; import javax.servlet.*; import java.io.*; public class ContainerTag extends TagSupport { // the session key for the view object private static final String VIEW_ATTR = "view"; // the name of the container private String name; // the view object, for use by ContainerIncludeTag private View view; // determine if the named view exists public int doStartTag( ) throws JspException { view = (View)pageContext.getRequest( ).getAttribute(VIEW_ATTR); if (!view.hasContainer(name)) return SKIP_BODY; return EVAL_BODY_INCLUDE; } // get the stored view public View getView( ) { return view; } // used by the JSP tag to set the name public void setName(String value) { name = value; } // get the name of this container public String getName( ) { return name; } } The ContainerIncludeTag simply uses the stored View object to map the include parameters into URIs. It then uses the JSP-provided pageContext object to include the view's content. Example 4-15 shows the ContainerIncludeTag. Example 4-15. ContainerIncludeTag.javaimport javax.servlet.jsp.tagext.*; import javax.servlet.jsp.*; import javax.servlet.*; import java.io.*; public class ContainerIncludeTag extends TagSupport { // the label of this include private String label; // get the view object from the parent tag // map the given name to a URL and include it public int doEndTag( ) throws JspException { // find the parent tag ContainerTag ct = (ContainerTag)findAncestorWithClass(this, ContainerTag.class); View v = ct.getView( ); // get the view URL String viewURL = v.getURL(ct.getName( ), label); if (viewURL != null) { try { // include it pageContext.include(viewURL); } catch( Exception ex ) { throw new JspException("Unable to include " + viewURL, ex); } } return EVAL_PAGE; } // used to set the name from the JSP tag public void setLabel(String value) { label = value; } } 4.3.3.5 Using templatesJust like in the Service to Worker example, the XML file format and implementation we have shown here is simplified. There are lots of possible extensions to this basic concept that can make templates a more convenient and powerful tool. One simple extension is to allow values to be substituted directly from XML attributes rather than from a file. For instance, in order to add titles to our web page in the previous example, we would rather not include a whole file with just the title in it. Instead, in the Views.xml file, we specify something like: <include label="title" text="Page 1" direct="true" /> The <cv:include> tag reads the direct attribute and substitutes the string "Page 1" instead of including an entire page. When developing a web application, using composite views promotes consistency in the user interface and reusability for views. As with decorators, composite views can cause trouble when they are deeply nested or have too many dependencies. In this chapter, we looked at three patterns that promote reuse in the presentation tier. All three use the same divide-and-conquer strategy, separating large, specialized components into smaller, resuable pieces. The Service to Worker pattern encapsulates model changes in reusable actions and navigation in a replaceable dispatcher. The View Helper pattern shows how to add reduce specialization in views by delegating tasks to a reusable helper, and the Composite View pattern divides the view into reusable containers and leaves. |
[ Team LiB ] |