Previous Section  < Day Day Up >  Next Section

15.3 Using Pure HTML Templates with XML View Definition Files

When you use JSP pages to create JSF views, you must use special elements (the JSF custom action elements as well as standard JSP directives and action elements). To look at the result, you need to let a web container process the page first instead of just opening the page in a browser. In addition, plain HTML-based development tools are not always able to deal with the special elements. Developing the views as regular Java classes—as described in the previous section—may be familiar to old GUI application gurus, but it doesn't work well when nonprogrammers develop the user interface layout.

The Apache Tapestry (http://jakarta.apache.org/tapestry/) open source web interface framework uses an approach in which each page is described by a combination of a page specification file, a Java class similar to a JSF "glue class" or "backing bean," and a plain HTML template file. The page specification file defines Tapestry components and binds them to elements in the HTML template identified by special ID attributes and to properties of the Java page class. This means that people skilled in user interface design and experienced with HTML, CSS, and so on, can develop the user interface with standard HTML development tools—they don't have to learn another set of special markup elements or worry about EL expressions and request processing lifecycles. Java programmers develop the page classes and the business logic classes, and tie the whole thing together through the page specification file.

With a custom ViewHandler, JSF can support this development model as well. In this section, we look at an embryo for such a ViewHandler implementation. I'm not making any claims that this example supports all the same features as Tapestry, just that it's similar in spirit. The custom ViewHandler can provide the base for a JSF environment on par with Tapestry, but to reach that point it needs to be extended with more code than I can describe in this book and it may also require custom versions of additional JSF classes.

15.3.1 Developing the HTML Template and the View Specification

This custom ViewHandler works with views described by a combination of an HTML template and a view specification file. The HTML template for the newsletter subscription view looks like this:

<html>

  <head>

    <title>Newsletter Subscription</title>

  </head>

  <body>

    <form id="form">

      <table>

        <tr>

          <td>Email Address:</td>

          <td><input id="form:emailAddr" size="50" /></td>

        </tr>

        <tr>

          <td>Newsletters:</td>

          <td>

            <span id="form:subs">

              <input type="checkbox" value="1">foo</input>

              <input type="checkbox" value="2">bar</input>

            </span>

          </td>

        </tr>

      </table>

      <input id="form:save" type="submit" value="Save" />

    </form>

  </body>

</html>

It's a plain XHTML file with id attributes for the elements that represent JSF components. To keep the example simple, I've used id attribute values with the JSF naming container syntax, but a more sophisticated implementation would make it possible to use plain values instead. The reason for using XHTML instead of HTML is also to keep the implementation simple. There are parsers that can parse HTML and present it to an application as XML, so a real implementation can support both HTML and XHTML without too many modifications.

For the checkboxes, I'm using a <span> element to give the whole group one id attribute, because the whole group is handled by one JSF component. The <input> elements within the <span> element are ignored when the view is rendered, but serves a purpose during the template design to see how the page looks like in a browser.

The view specification file is an XML file that looks like this for the newsletter view:

<view-specification>

  <component id="form" type="javax.faces.Form">

    <component id="emailAddr" type="javax.faces.Input" 

      value="#{subscr.emailAddr}" />

    <component id="subs" type="javax.faces.SelectMany" 

      rendererType="javax.faces.Checkbox" value="#{subscr.subscriptionIds}">

      <component type="javax.faces.SelectItem" itemValue="1" 

        itemLabel="JSF News" />

      <component type="javax.faces.SelectItem" itemValue="2" 

        itemLabel="IT Industry News" />

      <component type="javax.faces.SelectItem" itemValue="3"

        itemLabel="Company News" />

    </component>

    <component id="save" type="javax.faces.Command" value="Save" 

      action="#{subscrHandler.saveSubscriber}" />

  </component>

</view-specification>

A <view-specification> element encloses all the other elements. Each component is described by a <component> element. All <component> elements must have a type attribute with a JSF component type identifier as the value. An id attribute must be provided for components tied to elements in the template file, such as the form, input field, and checkbox list components in this example. For <component> elements representing child components not tied directly to the template, such as the javax.faces.SelectItem components providing the list of choices for the javax.faces.SelectMany component, the id attribute is optional.

All attributes other than id and type must correspond to properties of the specified component type or to attributes recognized by the component's renderer. The ViewHandler configures the components based on the attributes in the view specification file when it creates the view. When it renders the view, it uses the attributes defined for the corresponding elements in the template file to further configure the components. This means that you can specify all render-dependent attributes, such as CSS class and field size, in the template file so the template can be debugged by opening it directly in a browser.

The input components are bound to managed bean properties through JSF value binding expressions and the command component is bound to an action method by a method binding expression, the same as in a JSP page with JSF custom actions. Rendering the view results in the same screen as for the example in the previous section, shown in Figure 15-1.

15.3.2 Developing the ViewHandler

The com.mycompany.jsf.pl.XMLViewHandlerImlp supports this type of view, defined by the combination of a view specification class and an HTML template:

package com.mycompany.jsf.pl;



import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.io.OutputStreamWriter;

import java.util.Iterator;

import java.util.Stack;



import javax.faces.application.Application;

import javax.faces.application.ViewHandler;

import javax.faces.component.ActionSource;

import javax.faces.component.UIComponent;

import javax.faces.component.UISelectMany;

import javax.faces.component.UISelectOne;

import javax.faces.component.UIViewRoot;

import javax.faces.context.ExternalContext;

import javax.faces.context.FacesContext;

import javax.faces.context.ResponseWriter;

import javax.faces.el.PropertyNotFoundException;

import javax.faces.el.ValueBinding;

import javax.faces.el.MethodBinding;



import org.xml.sax.Attributes;

import org.xml.sax.SAXException;

import org.xml.sax.SAXParseException;

import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.SAXParserFactory;

import javax.xml.parsers.ParserConfigurationException;

import javax.xml.parsers.SAXParser;



public class XMLViewHandlerImpl extends ViewHandlerImpl {



    public XMLViewHandlerImpl(ViewHandler origViewHandler) {

        super(origViewHandler);

    }

The XMLViewHandlerImpl class extends the ClassViewHandlerImpl class we developed in the previous section and inherits all its public methods. The differences between the two are in the protected methods.

The protected createViewRoot() method looks like this:

protected UIViewRoot createViewRoot(FacesContext context, String viewId) {



    SAXParserFactory factory = SAXParserFactory.newInstance( );

    SAXParser saxParser = null;

    try {

        saxParser = factory.newSAXParser( );

    }

    catch (SAXException e) {

        throw new IllegalArgumentException(e.getMessage( ));

    }

    catch (ParserConfigurationException e) {

        throw new IllegalArgumentException(e.getMessage( ));

    }



    UIViewRoot viewRoot = new UIViewRoot( );

    viewRoot.setViewId(viewId);

    ExternalContext ec = context.getExternalContext( );

    InputStream viewSpecIS = null;



    DefaultHandler handler = 

        new ViewSpecHandler(context.getApplication( ), viewRoot);

    try {

        viewSpecIS = context.getExternalContext( ).

            getResourceAsStream(viewId + ".view");

        saxParser.parse(viewSpecIS, handler);



    } catch (SAXParseException e) {

        String msg = "View spec parsing error: " + e.getMessage( ) +

            " at line=" + e.getLineNumber( ) +

            " col=" + e.getColumnNumber( );

        throw new IllegalArgumentException(msg);

    } catch (Exception e) {

        String msg = "View spec parsing error: " + e.getMessage( );

        throw new IllegalArgumentException(msg);

    } finally {

        try {

            if (viewSpecIS != null) {

                viewSpecIS.close( );

            }

        } catch (IOException e) {}

    }

    return viewRoot;

}

The createViewRoot() method creates the view by parsing the view specification file, creating the components defined by the <component> elements, configuring them based on the attribute values, and arranging them in a tree structure based on the element nesting. It uses the standard Java XML parser API (JAXP) to create a SAX parser. Next, it creates a UIViewRoot for the view and passes it along with a reference to the JSF Application object to a new instance of the ViewSpecHandler inner class. This class is an extension of the org.xml.sax.helpers.DefaultHandler class, which is a class for handling SAX parser events.

The view specification must be stored in a file with a path corresponding to the view ID plus the .view extension. The createView() method obtains an input stream for this file from the ExternalContext and then asks the SAX parser to parse the file.

If you've never used a SAX parser before, you may wonder how all this works, but it's fairly straightforward. The SAX parser calls methods on its DefaultHandler instance when it encounters things like the start tag or end tag for an element, characters, or whitespace. The DefaultHandler provides default implementations for all methods, so an application specific subclass only needs to implement the methods for the events of interest.

The ViewSpecHandler class is just interested in start and end tags, so it implements only the startElement() and endElement() methods:

private static class ViewSpecHandler extends DefaultHandler {

    private Stack stack;

    private Application application;



    public ViewSpecHandler(Application application, UIComponent root) {

        this.application = application;

        stack = new Stack( );

        stack.push(root);

    }



    public void startElement(String namespaceURI, String lName, 

        String qName, Attributes attrs) throws SAXException {



        if ("component".equals(qName)) {

            UIComponent component = createComponent(application, attrs);

            ((UIComponent) stack.peek( )).getChildren( ).add(component);

            stack.push(component);

        }

    }



    public void endElement(String namespaceURI, String lName,

        String qName) throws SAXException {



        if ("component".equals(qName)) {

            stack.pop( );

        }

    }

The ViewSpecHandler constructor saves a reference to the Application objects, creates a java.util.Stack, and pushes the root component onto the stack.

The startElement() method checks if the element is a <component> element. If it is, startElement() calls createComponent( ) to create an instance, which it then adds as a child of the component at the top of the stack and then pushes onto the stack. A real implementation would also support other element types for attaching validators, converters, and listeners to the components. The endElement() method simply pops the top object off the stack.

The createComponent() method is doing the grunt work in the ViewSpecHandler class:

private UIComponent createComponent(Application application,

    Attributes attrs) {



    if (attrs == null || attrs.getValue("type") == null) {

        String msg = 

            "'component' element without 'type' attribute found";

        throw new IllegalArgumentException(msg);

    }



    String type = attrs.getValue("type");

    UIComponent component = application.createComponent(type);

    if (component == null) {

        String msg = "No component class registered for 'type' " +

            type;

        throw new IllegalArgumentException(msg);

    }



    for (int i = 0; i < attrs.getLength( ); i++) {

        String name = attrs.getLocalName(i);

        if ("".equals(name)) {

            name = attrs.getQName(i);

        }

        if ("type".equals(name)) {

            continue;

        }

        String value = attrs.getValue(i);

        if (value.startsWith("#{")) {

            if ("action".equals(name)) {

                MethodBinding mb =

                    Application.createMethodBinding(value, null);

                ((ActionSource) component).setAction(mb);

            }

            else {

                ValueBinding vb = 

                    application.createValueBinding(value);

                component.setValueBinding(name, vb);

            }

        }

        else {

            component.getAttributes( ).put(name, value);

        }

    }

    return component;

}

It first verifies that there's a type attribute for the <component> element and calls the Application createComponent() method to create a component of the specified type. It then uses the remainder of the attributes to configure the component.

If the attribute value starts with the JSF EL #{ delimiter, it creates either a MethodBinding or a ValueBinding and calls the appropriate component methods to set it. In this example, I only recognize the action attribute as an attribute that takes a MethodBinding, but a real implementation should instead use introspection to figure out if the expression is for a method or value binding. Attributes that don't have a JSF EL expression value are set through the component's generic attributes list. When all <component> element attributes have been processed, the configured component is returned.

The protected renderResponse() method is almost identical to the createViewRoot() method:

protected void renderResponse(FacesContext context, UIComponent component)

    throws IOException {



    SAXParserFactory factory = SAXParserFactory.newInstance( );

    SAXParser saxParser = null;

    try {

        saxParser = factory.newSAXParser( );

    }

    catch (SAXException e) {

        throw new IllegalArgumentException(e.getMessage( ));

    }

    catch (ParserConfigurationException e) {

        throw new IllegalArgumentException(e.getMessage( ));

    }



    UIViewRoot root = (UIViewRoot) component;

    String viewId = root.getViewId( );

    ExternalContext ec = context.getExternalContext( );

    InputStream templIS = null;



    DefaultHandler handler = new TemplateHandler(context, root);

    try {

        templIS = context.getExternalContext( ).

            getResourceAsStream(viewId + ".html");

        saxParser.parse(templIS, handler);



    } catch (SAXParseException e) {

        String msg = "Template parsing error: " + e.getMessage( ) +

            " at line=" + e.getLineNumber( ) +

            " col=" + e.getColumnNumber( );

        throw new IllegalArgumentException(msg);

    } catch (Exception e) {

        String msg = "Template parsing error: " + e.getMessage( );

        throw new IllegalArgumentException(msg);

    } finally {

        try {

            if (templIS != null) {

                templIS.close( );

            }

        } catch (IOException e) {}

    }

}

The differences are that it parses the template file—stored with a path corresponding to the view ID plus the .html extension—with an instance of the TemplateHandler class, and that it uses the existing component tree instead of creating it.

The TemplateHandler class handles the start and end tag events for all elements in the template file, plus the events for characters in template element bodies.

private static class TemplateHandler extends DefaultHandler {

    private StringBuffer textBuff = null;

    private FacesContext context;

    private ResponseWriter out;

    private UIViewRoot root;

    private Stack stack;

    private boolean suppressTemplate;



    public TemplateHandler(FacesContext context, UIViewRoot root) {



        this.context = context;

        this.root = root;

        out = context.getResponseWriter( );

        stack = new Stack( );

        stack.push(root);

    }

The constructor saves references to the FacesContext and the view's UIViewRoot, and gets a reference to the ResponseWriter. It also creates a Stack and pushes the root component onto the top.

The SAX parser calls a handler method named characters( ) when it encounters characters in element bodies:

    public void characters(char buf[], int offset, int len)

        throws SAXException {



        if (suppressTemplate) {

            return;

        }

            

        if (textBuff == null) {

            textBuff = new StringBuffer(len * 2);

        }

        textBuff.append(buf, offset, len);

    }



    private void handleTextIfNeeded( ) {

        if (textBuff != null) {

            String value = textBuff.toString( ).trim( );

            textBuff = null;

            if (value.length( ) == 0) {

                return;

            }

            try {

                out.writeText(value, null);

            }

            catch (IOException ioe) {}

        }

    }

}

The characters() method saves the characters in a StringBuffer, unless the template content is currently suppressed (a situation I'll talk more about in a bit). The other event handling methods call the private handleTextIfNeeded() method to add buffered characters to the response.

The startElement() method looks like this:

public void startElement(String namespaceURI, String lName, 

    String qName, Attributes attrs) throws SAXException {



    handleTextIfNeeded( );



    String id = attrs.getValue("id");

    if (id != null && root.findComponent(id) != null) {

        UIComponent comp = findAndConfigure(id, attrs);

        stack.push(comp);

        try {

            comp.encodeBegin(context);

        }

        catch (IOException ioe) {}

        suppressTemplate = suppressTemplate(comp);

    }

    else {

        stack.push(qName);

        if (!suppressTemplate) {

            try {

                out.startElement(qName, null);

                for (int i = 0; i < attrs.getLength( ); i++) {

                    out.writeAttribute(attrs.getQName(i), 

                         attrs.getValue(i), null);

                }

            }

            catch (IOException ioe) {}

        }

    }

}

The startElement() method calls the handleTextIfNeeded() method to add buffered text to the response, if any. Next, it looks for an id attribute. If it finds one, the template element may be bound to a component with this ID. The startElement() calls the findComponent() method on the root component method with the ID to locate the component, and if it finds one, it calls the findAndConfigure() method to configure it based on the template element attributes. It then pushes the component onto the stack and calls its encodeBegin( ) method to let it render itself.

The findAndConfigure() method looks like this:

private UIComponent findAndConfigure(String id, Attributes attrs) {

    UIComponent comp = root.findComponent(id);

    for (int i = 0; i < attrs.getLength( ); i++) {

        // Don't overwrite "id"

        if ("id".equals(attrs.getQName(i))) {

            continue;

        }

        comp.getAttributes( ).put(attrs.getQName(i), attrs.getValue(i));

    }

    return comp;

}

It locates the component and sets all attributes from the template element, except the id attribute. The id attributes in the template have values with the naming container client ID syntax, so they must not be used to override the components' real IDs.

After calling the encodeBegin() on the component, the startElement() method sets a suppressTemplate variable to the value returned by the suppressTemplate() method:

private boolean suppressTemplate(UIComponent comp) {

    return comp.getRendersChildren( ) || 

        comp instanceof UISelectMany || comp instanceof UISelectOne;

}

This method returns true if the component renders its children. A call to getRendersChildren() should be enough, but (possibly due to a specification bug) this method returns false for components of type UISelectMany and UISelectOne, even though they in fact do render their own children.

The example ViewHandler ignores all of the content of a template element bound to a component that renders its children; for instance, the template <input> elements in the body of the <span> element for the UISelectMany component in the newsletter subscription template file. A more sophisticated implementation could possibly use the template element body to configure the main component, e.g., decide which type of renderer it should use.

The remainder of the startElement() method deals with template elements without id attributes and elements with id attributes that don't match a component in the view. For these elements, the element name is pushed onto the stack and if suppressTemplate is false, the element and all its attributes are copied as is to the response.

The endElement() method takes care of the rest of the rendering requirements:

        public void endElement(String namespaceURI, String lName,

            String qName) throws SAXException {



            handleTextIfNeeded( );



            Object o = stack.pop( );

            if (o instanceof String) {

                try {

                    out.endElement(qName);

                    out.writeText("\n", null);

                }

                catch (IOException ioe) {}

            }

            else {

                UIComponent comp = (UIComponent) o;

                try {

                    if (comp.getRendersChildren( )) {

                        comp.encodeChildren(context);

                    }

                    comp.encodeEnd(context);

                    out.writeText("\n", null);

                }

                catch (IOException ioe) {}

                if (suppressTemplate) {

                    suppressTemplate = false;

                }

            }

        }

    }

}

It first calls the handleTextIfNeeded() method and then pops the top object off the stack. If the object is a String, the end tag must be for a template element, not bound to a component, so it's just added to the response.

If it's not a String, it must be a component. The endElement() method calls its encodeChildren() method if the component's getRenderersChildren( ) method returns true. Then the encodeEnd() method is called to finish the rendering of the component. When the component has been rendered, the suppressTemplate flag is reset if it was set to start processing content from the template again.

The XMLViewHandlerImpl class described in this section provides the basic functionality needed to support the use of pure HTML templates with the component configuration in separate view specification files. A number of enhancements must be made before it can be used for real products; for instance, more thorough error handling is needed, view specification elements for facets, validators, converters, and listeners must be added and supported by the SAX parser handlers, and elements for including pieces from an external view specification and template file combination àla Tapestry's component specification files is essential. A special render kit and a slightly different rendering model may also be needed to match some of Tapestry's features (such as clean support for generation of client-side validation code). Performance enhancements, e.g., in the form of cached templates, are also possible.

The ClassViewHandlerImpl class described in the first section of this chapter can also be improved, but these two examples illustrate the possibilities and I hope might inspire someone to pick up where I left off and develop a useful alternative to the JSP layer that may get the official blessing in a future version of the JSF specification.

    Previous Section  < Day Day Up >  Next Section