Previous Section  < Day Day Up >  Next Section

15.2 Using Java Classes as Views

As you've seen, JSF components are implemented as regular Java classes extending the javax.faces.component.UIComponent class, and can be instantiated and manipulated programmatically. The JSF component actions you use in the JSP pages create instances of these classes and configure them based on the custom action attributes, as discussed in Chapter 13 and 14.

If you come from a standalone GUI development background, working directly with instances of component classes may feel more familiar than messing around with special elements in a JSP page. The first custom ViewHandler we look at supports this development model with JSF views implemented as regular Java classes, similar to the classes used for a Java Swing interface.

15.2.1 Developing the View Class

Before we look at the custom ViewHandler, let's look at a class that creates a view. The com.mycompany.newsservice.views.SubscribeView class creates a view that's identical to the newsletter subscription example in Chapter 2:

package com.mycompany.newsservice.views;



import javax.faces.application.Application;

import javax.faces.component.UICommand;

import javax.faces.component.UIForm;

import javax.faces.component.UIInput;

import javax.faces.component.UIOutput;

import javax.faces.component.UIPanel;

import javax.faces.component.UISelectItems;

import javax.faces.component.UISelectMany;

import javax.faces.component.UIViewRoot;

import javax.faces.context.FacesContext;

import javax.faces.model.SelectItem;

import javax.faces.el.MethodBinding;

import javax.faces.el.ValueBinding;



import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;



import com.mycompany.jsf.pl.View;



public class SubscribeView implements View {

    public UIViewRoot createView(FacesContext context) {

        Application application = context.getApplication( );

        UIViewRoot viewRoot = new UIViewRoot( );



        UIForm form = new UIForm( );

        viewRoot.getChildren( ).add(form);



        UIPanel grid = new UIPanel( );

        grid.setRendererType("javax.faces.Grid");

        grid.getAttributes( ).put("columns", "2");



        UIOutput emailLabel = new UIOutput( );

        emailLabel.setValue("Email Address:");

        grid.getChildren( ).add(emailLabel);

        UIInput email = new UIInput( );

        ValueBinding emailAddr = 

            application.createValueBinding("#{subscr.emailAddr}");

        email.setValueBinding("value", emailAddr);

        grid.getChildren( ).add(email);



        UIOutput subsLabel = new UIOutput( );

        subsLabel.setValue("Newsletters:");

        grid.getChildren( ).add(subsLabel);

        UISelectMany subs = new UISelectMany( );

        subs.setRendererType("javax.faces.Checkbox");

        UISelectItems sis = new UISelectItems( );

        List choices = new ArrayList( );

        choices.add(new SelectItem("1", "JSF News"));

        choices.add(new SelectItem("2", "IT Industry News"));

        choices.add(new SelectItem("3", "Company News"));

        sis.setValue(choices);

        subs.getChildren( ).add(sis);

        grid.getChildren( ).add(subs);

        form.getChildren( ).add(grid);



        UICommand command = new UICommand( );

        command.setValue("Save");

        MethodBinding action = 

            application.createMethodBinding("#{subscrHandler.saveSubscriber}",

                null);

        command.setAction(action);

        form.getChildren( ).add(command);

        viewRoot.getChildren( ).add(form);



        return viewRoot;

    }

}

The SubscribeView class implements an interface named com.mycompany.jsf.pl.View, which declares a single method named createView(). This method creates and returns an instance of UIViewRoot with components of other types as its children. Declaring the method in an interface that all view classes must implement makes it possible for the ViewHandler to work with any view class without knowing its name.

The createView() method in the SubscribeView class creates a UIForm component containing a UIPanel component configured with a grid renderer for layout. Children of type UIOutput for text labels, a UIInput component for the email address, and a UISelectMany component with a checkbox renderer for the list of newsletters are added as children of the UIPanel component. The child component created for the UISelectMany component is a UISelectItems component with SelectItem instances for each newsletter choices. The final UIForm child is a UICommand component for submitting the form.

The UIInput and UISelectMany components are bound to the same managed bean properties as we used for the JSP version of this view in Chapter 2. The ValueBinding objects are created by calling the Application createValueBinding( ) method with the same kind of JSF value binding expressions as for the JSP layer. The UICommand component is bound to an action method in a similar manner, with a MethodBinding object created by the Application createMethodBinding( ) method.

Rendering the view created by the SubscribeView class results in the screen shown in Figure 15-1.

Figure 15-1. The screen generated from the SubscribeView
figs/Jsf_1501.gif

It works just like its JSP counterpart, so run the example, add a value in the email field, select a few checkboxes, click the button, and verify that the method in the SubscriberHandler class is invoked just as in Chapter 2, writing the email address and the current selections to the shell where the web container runs.

In this example, I don't use any converters or validators, but such objects can of course be added to the components using the methods you've seen in the previous chapters. For some applications it may even make sense to handle events with listeners implemented as anonymous inner classes, as is common in a Java Swing application, instead of binding the components to managed bean methods.

15.2.2 Developing the ViewHandler

Let's move on to the custom ViewHandler that supports views represented by regular Java classes like the SubscribeView class. The class declaration and the constructor look like this:

package com.mycompany.jsf.pl;



import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.io.OutputStreamWriter;

import java.util.HashMap;

import java.util.Iterator;

import java.util.Locale;

import java.util.Map;

import javax.faces.FactoryFinder;

import javax.faces.application.StateManager;

import javax.faces.application.StateManager.SerializedView;

import javax.faces.application.ViewHandler;

import javax.faces.component.UIComponent;

import javax.faces.component.UIViewRoot;

import javax.faces.context.ExternalContext;

import javax.faces.context.FacesContext;

import javax.faces.context.ResponseWriter;

import javax.faces.render.RenderKit;

import javax.faces.render.RenderKitFactory;

import javax.servlet.ServletRequest;

import javax.servlet.ServletResponse;

import com.mycompany.newsservice.views.SubscribeView;



public class ClassViewHandlerImpl extends ViewHandler {

    private static final String STATE_VAR = "com.mycompany.viewState";

    protected ViewHandler origViewHandler;

    private Map views = new HashMap( );



    public ClassViewHandlerImpl(ViewHandler origViewHandler) {

        this.origViewHandler = origViewHandler;

    }

The com.mycompany.jsf.pl.ClassViewHandlerImpl extends the abstract ViewHandler class. The constructor takes an argument of type ViewHandler and saves a reference to it in an instance variable. When a pluggable class, such as a ViewHandler implementation, has a constructor with an argument of the same type as the object it replaces, JSF uses this constructor to give it a reference to the previously registered object. This makes it easy for a custom class to delegate most of the implementation to the previously registered object. I use this feature to delegate the processing of a number of methods in the ClassViewHandlerImpl:

public Locale calculateLocale(FacesContext context) {

    return origViewHandler.calculateLocale(context);

}



public String calculateRenderKitId(FacesContext context) {

    return origViewHandler.calculateRenderKitId(context);

}



public String getActionURL(FacesContext context, String viewId) {

    return origViewHandler.getActionURL(context, viewId);

}



public String getResourceURL(FacesContext context, String path) {

    return origViewHandler.getResourceURL(context, path);

}

The custom class doesn't modify the behavior for these methods, so I let the previous ViewHandler implementation handle them instead. You can look at the description of these methods in Appendix D for details, but briefly, the default calculateLocale() determines the locale based on the Accept-Language header as described in Chapter 11, calculateRenderKitId() returns either the ID for the JSF default render kit or the render kit ID specified in the faces-config.xml file, getActionURL() returns a URL for invoking the specified view, and getResourceURL() returns a URL for a resource within the web application (e.g., an image file).

The first customized method is the createView() method:

public UIViewRoot createView(FacesContext context, String viewId) {

    String realViewId = viewId;

    if (viewId.indexOf(".") != -1) {

        realViewId = viewId.substring(0, viewId.indexOf("."));

    }

    UIViewRoot viewRoot = createViewRoot(context, realViewId);

    if (viewRoot != null) {

        if (context.getViewRoot( ) != null) {

            UIViewRoot oldRoot = context.getViewRoot( );

            viewRoot.setLocale(oldRoot.getLocale( ));

            viewRoot.setRenderKitId(oldRoot.getRenderKitId( ));

        }

        else {

            ViewHandler activeVH =

                context.getApplication( ).getViewHandler( );

            viewRoot.setLocale(activeVH.calculateLocale(context));

            viewRoot.setRenderKitId(activeVH.calculateRenderKitId(context));

        }

    }

    return viewRoot;

}

JSF calls this method to create a new view when it can't find a previously saved view state for the requested view or when the processing of a navigation rule results in the selection of a new view.

The viewId parameter identifies the view to create. The ViewHandler implementation defines exactly what a view ID is, but it's typically a part of the request URI path, possibly modified to be the same independent of whether JSF is mapped to a URI prefix (e.g., /faces/*) or an extension (e.g., *.faces). To use a concrete example, the ViewHandler for the JSP layer uses the context-relative path for the JSP page as the view ID. Because the JSF specification doesn't define a public method for converting a request path to a view ID, the createView( ) method is invoked with the context-relative path instead (despite the parameter name) and it's up to the createView() method to do the conversion between a path and a view ID. The JSP layer ViewHandler converts the request path to a context-relative JSP page path by either appending the JSP extension (when prefix mapping is used) or replacing the request path extension with the JSP extension (when extension mapping is used). The custom ClassViewHandlerImpl class simply drops the extension if there is one and uses the rest of the path as the view ID.

After adjusting the path to a viewId value, the createView() method calls the createViewRoot() method to create the view and then sets the locale and render-kit ID for the returned UIViewRoot. This information is copied from the previous view, if any, or calculated by calling the calculateLocale() and calculateRenderKitId() methods on the ViewHandler returned by the Application instance It's important to delegate to the handler returned by the Application instance, because it's the one most recently registered and it may customize the implementation of these methods but delegate the rest of the processing to the previously registered handler.

The createViewRoot() method uses a View implementation like the SubscribeView class to create the component tree for the view:

protected UIViewRoot createViewRoot(FacesContext context, String viewId) {

    UIViewRoot viewRoot = null;

    View view = (View) views.get(viewId);

    if (view == null) {

        if ("/subscribe".equals(viewId)) {

            view = new SubscribeView( );

            views.put(viewId, view);

        }

    }

    if (view != null) {

        viewRoot = view.createView(context);

        viewRoot.setViewId(viewId);

    }

    return viewRoot;

}

The ClassViewHandlerImpl class uses a java.util.Map as a cache for View instances, so the createViewRoot( ) method first tries to get hold of an instance from the cache. If it can't find one, it creates an instance of the View class registered for the view ID and puts it in the cache. In this example ViewHandler implementation, I have hardcoded the class name for the one View class it supports, but a real implementation could be configured with mappings between view IDs and implementation classes instead. With a reference to a cached or newly created View instance, the createViewRoot( ) method calls the createView() method on the View, sets the viewId property on the returned UIViewRoot, and returns it.

JSF calls the renderView() method when it's time to render the view:

public void renderView(FacesContext context, UIViewRoot viewToRender) 

    throws IOException {



    setupResponseWriter(context);



    StateManager sm = context.getApplication( ).getStateManager( );

    SerializedView state = sm.saveSerializedView(context);

    context.getExternalContext( ).getRequestMap( ).put(STATE_VAR, state);



    context.getResponseWriter( ).startDocument( );

    renderResponse(context, viewToRender);

    context.getResponseWriter( ).endDocument( );

}

The ResponseWriter all renderers use to generate the markup for the components is created and configured by a call to the setupResponseWriter() method:

private void setupResponseWriter(FacesContext context) 

    throws IOException {



    ServletResponse response = (ServletResponse)

        context.getExternalContext( ).getResponse( );

    OutputStream os = response.getOutputStream( );

    Map headers = context.getExternalContext( ).getRequestHeaderMap( );

    String acceptHeader = (String) headers.get("Accept");



    RenderKitFactory renderFactory = (RenderKitFactory)

        FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);

    RenderKit renderKit = 

        renderFactory.getRenderKit(context,

             context.getViewRoot( ).getRenderKitId( ));

    ResponseWriter writer = 

        renderKit.createResponseWriter(new OutputStreamWriter(os),

            acceptHeader, response.getCharacterEncoding( ));

    context.setResponseWriter(writer);

    response.setContentType(writer.getContentType( ));

}

A web container uses the ServletResponse object returned by the ExternalContext getResponse() method to generate the response. The setupResponseWriter() method gets hold of the ServletResponse object and retrieves the java.io.OutputStream for the response body. It then extracts the value of the Accept request header, containing a comma-separated list of the content MIME types (e.g., "text/html, text/plain") the client accepts.

The ResponseWriter writes markup elements, so different implementation classes may be needed for different markup languages (e.g., one for XML languages and one for HTML). The JSF class that knows the details about a specific markup language is the RenderKit, so the setupResponseWriter( ) method creates an appropriate ResponseWriter by retrieving the RenderKit for the view and calling its createResponseWriter() method with the Accept header value and a java.io.OutputStreamWriter wrapped around the response body output stream. A fancy render kit implementation may use the Accept header value to return a ResponseWriter instance configured to produce slightly different element syntax depending on the header value, e.g., use strict XML syntax if the header value contains "application/xhtml+xml" (the MIME type for XHTML) or use plain old HTML syntax if it contains only "text/html". The ResponseWriter getContentType( ) always returns the MIME type it's compliant with, so the setupResponseWriter() method uses this value to set the content type for the ServletResponse object.

Returning to the renderView() method, the view state is collected and returned as an instance of the SerializedView class by the StateManager saveSerializedView( ) method. This method collects the state for all components by calling their saveState() methods (as I described in Chapter 14) and traverses the component tree to figure out parent-child relationships for all components. It encodes this information in an implementation-dependent way and returns it as an instance of the ServializedView class. The renderView( ) method saves the state as a request scope variable for later and calls the protected renderResponse() method to render the components:

protected void renderResponse(FacesContext context, UIComponent component)

    throws IOException {



    component.encodeBegin(context);

    if (component.getRendersChildren( )) {

        component.encodeChildren(context);

    }

    else {

        Iterator i = component.getChildren( ).iterator( );

        while (i.hasNext( )) {

            renderView(context, (UIComponent) i.next( ), state);

        }

    }

    component.encodeEnd(context);

}

The renderResponse() method calls encodeBegin(), encodeChildren( ) for components that render their own children, and encodeEnd() recursively on all components in the component tree.

Renderers for the UIForm component call the ViewHandler writeState() method just before they render the end tag for the form:

public void writeState(FacesContext context) {

    SerializedView state = (SerializedView)

        context.getExternalContext( ).getRequestMap( ).get(STATE_VAR);

    if (state != null) {

        StateManager sm = context.getApplication( ).getStateManager( );

        sm.writeState(context, state);

    }

}

For ClassViewHandler, this method calls the StateManager writeState() method with the state saved as a request scope attribute value by the renderView() method. The writeState( ) method in the JSF reference implementation's default StateManager writes a complete HTML element for a hidden field containing the state if client-side state is enabled, which works fine for this example custom ViewHandler. Other JSF implementations may implement the writeState() method differently, e.g., writing just the encoded state value to a previously created element, because the JSF spec leaves these details up to each vendor. To be on the safe side, it's a good idea to implement a custom StateManager along with the custom ViewHandler, to ensure that they are in sync, but I rely on the reference implementation behavior for this example.

This roundabout way of getting the state added to the response is needed primarily for the JSP layer. When the JSP presentation layer is used, the component tree is created at the same time as it's rendered, so the state can't be collected until at the end of the rendering phase. The JSP layer's ViewHandler therefore buffers the response and writes a marker in the buffered response[1] every time the writeState() method is called. When the whole tree has been created and rendered to the buffer, it asks the StateManager to collect the state and then goes back and replaces the markers with the hidden fields for the state by calling the StateManager writeState( ) method.

[1] Instead of writing a marker, an implementation can make a note about where in the buffer the state should be inserted.

The restoreView() method is called to restore a view from the state information included with the request for client-side state or available somewhere on the server for server-side state:

public UIViewRoot restoreView(FacesContext context, String viewId) {

   String realViewId = viewId;

   if (viewId.indexOf(".") != -1) {

      realViewId = viewId.substring(0, viewId.indexOf("."));

   }



   String renderKitId =

      context.getApplication( ).getViewHandler( ).

      calculateRenderKitId(context);



   StateManager sm = context.getApplication( ).getStateManager( );

   return sm.restoreView(context, realViewId, renderKitId);

}

The viewId parameter may hold either a URI path or a real viewId, so it's adjusted the same way as in the createView() method.

With client-side state, only the render kit knows how the state was encoded in the response[2] and, therefore, how to pick it up from the request data, so the restoreView() method must first figure out which render kit the application uses. It does so by calling the calculateRenderKit( ) method of the most recently registered ViewHandler instance, just as in the createView( ) method shown earlier. If the application doesn't specify a render kit, the default JSF HTML render kit is used.

[2] The StateManager cooperates with a class named javax.faces.render.ResponseStateManager that belongs to the render kit for encoding the state in a response, as well as for extracting it from request data.

The restoreView() method asks the StateManager to restore the view, potentially with the help from the render kit, and returns the UIViewRoot for the view or null if no state is available for the view. The JSF implementation then processes the returned view as we've talked about in previous chapters.

    Previous Section  < Day Day Up >  Next Section