Skip to main content

Auto-save JSF forms with Ajax: Part 2

Manage form data on the server side

Andrei Cioroianu, Senior Java Developer and Consultant, Devsphere
Andrei Cioroianu is a Senior Java Developer and Consultant at Devsphere, a provider of custom Java EE development and Ajax/JSF consulting services. You can reach Andrei through the contact form at www.devsphere.com.

Summary:  In the first part of this "Auto-save JSF forms with Ajax" series, author and Java™ developer Andrei Cioroianu showed you how to build Java applications that automatically save Web forms, using Asynchronous JavaScript + XML (Ajax) and JavaServer Faces (JSF) technologies. You learned how to obtain, encode, and submit form data with JavaScript and XMLHttpRequest, how to adapt the JSF request processing life cycle for handling Ajax requests, and how to get the submitted data from the JSF component tree on the server side. In this second installment of the three-part series, you will see how to identify anonymous users across browser sessions, how to manage the automatically saved form data for multiple users and pages, how to choose a data repository, and how to deal with thread-safety issues.

View more content in this series

Date:  18 Sep 2007
Level:  Intermediate
Activity:  2275 views

Introduction

Part 1 of this series describes a scenario in which your application automatically saves form data on the server and you want to be able to restore forms after your users close and reopen their browsers. The solution works even if the user's browser crashes, or if the user leaves the application without clicking Submit on the Web form.

The sample application included with this article (see Download) contains a typical JSF form named SupportForm.jsp whose data is periodically submitted to the server using the JavaScript functions of the AutoSaveScript.js file. Both SupportForm.jsp and AutoSaveScript.js are described in Part 1, which also shows how to use a JSF phase listener to process Ajax requests without interfering with the application logic.

In this article, you'll learn how to build a thread-safe data repository for keeping auto-saved form data. You'll see how to choose data structures, how to populate them with the form data extracted from the JSF component tree, how to restore the state of the JSF components, how to limit the memory resources of the data repository, and how to implement its persistence. You'll also learn several Web techniques, such as using filters and browser ID cookies.

Identifying users across sessions

The developerWorks Ajax resource center

Check out the Ajax Resource Center, your one-stop shop for information about the Ajax programming model, including articles and tutorials, discussion forums, blogs, wikis, events, and news. If it's happening, it's covered here.

To restore Web forms after users close and reopen their browsers, the application needs to identify users across sessions, which is easy if users are authenticated. If your application uses standard methods for authenticating users, you can call the getUserPrincipal() method defined by the HttpServletRequest interface and then obtain the user name with the getName() method of java.security.Principal.

If your application must support form-saving and form-restoring features for anonymous users, as well, you can set a browser ID, which is very similar to the session ID that tracks the user during a single session. In fact, you can take the value of the session ID cookie and set another cookie named BROWSERID when the user accesses the application for the first time. Unlike the session ID, which is generated at the beginning of a session and expires at the end of the session, the BROWSERID cookie is set only once and can expire after a long period, such as several years.

Your users will thank you

Read on to see how building auto-saving capabilities into your Ajax applications makes your user's Web experience convenient and efficient. And along the way, pick up a few advanced Web techniques, like using filters and browser ID cookies.

Using a servlet filter

A servlet filter is the right place for setting the BROWSERID cookie because the filter can intercept every HTTP request, adding the cookie to the HTTP response if the user accesses the application for the first time. Once the browser receives the cookie through the first response, any subsequent request will contain the BROWSERID cookie, allowing the application to identify the anonymous user though his browser ID. The sample code that accompanies this article contains a class named BrowserIdFilter, which implements javax.servlet.Filter. This class also has a method named getBrowserId() (see Listing 1), which iterates over the cookies of a request object, returning the value of the BROWSERID cookie or null if such a cookie doesn't exist.


Listing 1. Getting the browser ID cookie
                
package autosave;
...
import javax.servlet.http.Cookie;
...
public class BrowserIdFilter implements Filter {
    public static String BROWSERID = "BROWSERID"; // cookie name

    public static String getBrowserId(HttpServletRequest httpRequest) {
        String browserId = null;
        Cookie cookies[] = httpRequest.getCookies();
        if (cookies != null)
            for (int i = 0; i < cookies.length; i++) {
                if (BROWSERID.equals(cookies[i].getName())) {
                    browserId = cookies[i].getValue();
                    break;
                }
            }
        return browserId;
    }
    ...
}

Listing 2 shows the doFilter() method of BrowserIdFilter, which uses getBrowserId() to test if the cookie hasn't already been set. If getBrowserId() returns null, doFilter() gets the session object with getSession() and the session ID with getId(). Then, doFilter() creates a Cookie object, sets the maxAge and path properties, and adds the cookie to the response object. To ensure the proper processing of the HTTP request, the request and response objects are passed to the doFilter() method of the chain parameter.


Listing 2. Implementing the doFilter() method of the Filter interface
                
package autosave;
...
import javax.servlet.Filter;
import javax.servlet.FilterChain;
...
public class BrowserIdFilter implements Filter {
    public static String BROWSERID = "BROWSERID"; // cookie name
    public static int IDAGE = 3600 * 24 * 365 * 3; // three years
    ...
    public void doFilter(ServletRequest request,
            ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        if (getBrowserId(httpRequest) == null) {
            // The BROWSERID cookie has not been found. This must be
            // the first time the user accesses the application.
            // Use the current session's ID as the value for
            // the BROWSERID cookie.
            HttpServletResponse httpResponse
                = (HttpServletResponse) response;
            String browserId = httpRequest.getSession().getId();
            Cookie browserCookie = new Cookie(BROWSERID, browserId);
            browserCookie.setMaxAge(IDAGE);
            browserCookie.setPath(httpRequest.getContextPath());
            httpResponse.addCookie(browserCookie);
        }
        chain.doFilter(request, response);
    }
    ...
}

Configuring the servlet filter

Listing 3 shows how to configure the BrowserIdFilter class in web.xml so the filter can intercept every HTTP request.


Listing 3. Configuring the filter
                
<web-app ...>
    ...
    <filter>
        <filter-name>BrowserIdFilter</filter-name>
        <filter-class>autosave.BrowserIdFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>BrowserIdFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
</web-app>

Set a browser ID

The browser ID can also be used for authenticated users so they don't have to log in each time they access an application. You've probably seen Web sites where the login screens have checkboxes labeled "Remember me on this computer." Well, such a checkbox authorizes the site to set a browser ID so you don't have to log in again when returning to the site.

The browser ID solution should be used only if your forms do not contain sensitive information because all users accessing the application through a shared computer would actually be treated as a single user. The only secure way to identify users across sessions is to use standard authentication methods based on user names and passwords. However, the password-based authentication has the inconvenience that users must register to the application. In many cases, security is very important, but sometimes users might prefer to remain anonymous instead of registering to a Web site. The browser ID offers an easy way to track anonymous users.

The form auto-saving feature presented in this series works with authenticated and anonymous users. In the following sections, you'll see how to store and retrieve the auto-saved form data and how to work with it in a multithreaded environment.


Selecting a data repository

First, you must choose a data structure and a repository for keeping the form data. This is an important decision because the repository will be accessed frequently for storing temporary data. In the sample application, data is automatically saved every 10 seconds for every form instance, but in a real application, it would be reasonable to increase this interval up to 10 minutes if you have a large number of concurrent users.

Memory is the obvious place for storing the auto-saved form data because most of this data is stored only for a short time, then it is replaced by fresher data. Each form instance saves its data periodically until the user submits the form. Any temporary data associated with a submitted form should be flushed from the memory. If the user cannot submit the form or if he abandons the page without clicking the Submit button, the last saved data is kept in memory as long as possible. When the user returns to the form, he will have the option to restore the form if the saved data is still available.

Saving and restoring the values of the JSF components

The auto-saved data of every form instance is kept in a Map<String, Object> instance. Each entry and element of such a data map contains the value of a JSF input component whose ID is also the key in the data map. This structure is similar to the parameter map of javax.servlet.ServletRequest, but it is not the same because the request parameter map contains string arrays, while the repository data map keeps the converted and validated values of all input components of a JSF view. The saveValues() method of the DataMapRepository class is a recursive method that traverses a JSF component tree, populating a data map with the values of the input components that implement EditableValueHolder (see Listing 4).


Listing 4. Storing the values of the JSF input components into a data map
                
package autosave;
...
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
...
public class DataMapRepository ... {
    ...
    public static void saveValues(UIComponent comp,
            Map<String, Object> dataMap) {
        if (comp == null)
            return;
        if (comp instanceof EditableValueHolder) {
            // Input component. Put its value into the data map
            EditableValueHolder evh = (EditableValueHolder) comp;
            dataMap.put(comp.getId(), evh.getValue());
        }
        // Iterate over the children of the current component
        Iterator children = comp.getChildren().iterator();
        while (children.hasNext()) {
            UIComponent child = (UIComponent) children.next();
            // Recursive call
            saveValues(child, dataMap);
        }
    }
    ...
}

The restoreValues() method (shown in Listing 5) traverses the JSF component tree, restoring the values of the input components. This method also clears the submittedValue property of each EditableValueHolder component so the form's data can be restored to the data from the map, ignoring any submitted data. The saveValues() method is used later in this article and restoreValues() in Part 3 of this series.


Listing 5. Restoring the values of the JSF input components
                
package autosave;
...
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
...
public class DataMapRepository ... {
    ...
    public static void restoreValues(UIComponent comp,
            Map<String, Object> dataMap) {
        if (comp == null || dataMap == null)
            return;
        if (comp instanceof EditableValueHolder) {
            // Input component. Get its value from the data map
            // and clear any submitted value
            EditableValueHolder evh = (EditableValueHolder) comp;
            evh.setValue(dataMap.get(comp.getId()));
            evh.setSubmittedValue(null);
        }
        // Iterate over the children of the current component
        Iterator children = comp.getChildren().iterator();
        while (children.hasNext()) {
            UIComponent child = (UIComponent) children.next();
            // Recursive call
            restoreValues(child, dataMap);
        }
    }

}

Each data map instance has a unique ID composed from the user ID and the JSF view ID. Therefore, it is natural to keep all these data maps in a repository map. The DataMapRepository class extends java.util.LinkedHashMap and has a method named getDataMapId() (see Listing 6) that generates the ID of a data map, using the given faces context whose methods return objects containing the necessary user and view information.

If the user is logged in, getDataMapId() obtains the user name from the Principal object. Otherwise, the user is anonymous and getDataMapId() uses the browser ID. The JSF view ID, which is part of the returned ID, is unique for each JSF page. Therefore, the data map ID is unique for each user and page combination.


Listing 6. Generating the unique ID of a data map
                
package autosave;
...
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
...
import java.security.Principal;
...
public class DataMapRepository ... {
    ...
    public static String getDataMapId(FacesContext ctx) {
        UIViewRoot root = ctx.getViewRoot();
        if (root == null)
            return null;
        ExternalContext ectx = ctx.getExternalContext();
        String userId = null;
        Principal principal = ectx.getUserPrincipal();
        if (principal != null) {
            // Use the name of the authenticated user.
            userId = principal.getName();
        } else {
            // Use the browser ID of the anonymous user.
            userId = BrowserIdFilter.getBrowserId(
                    (HttpServletRequest) ectx.getRequest());
        }
        if (userId == null)
            return null;
        // Concatenate the user ID and the JSF view ID
        return userId + root.getViewId();
    }
    ...
}

Limiting the memory resources of the data repository

Some restrictions are necessary because the Java heap is limited and the data repository shouldn't consume too much memory. In the sample application, the repository has a limit of data map instances it can store. When this limit is reached, the oldest data map is removed from the repository and becomes eligible for garbage collection. This mechanism is already built into the java.util.LinkedHashMap class — you just have to override the removeEldestEntry() method to return true when the repository has more entries than the maximum allowed number.

Listing 7 shows the DataMapRepository class, which extends LinkedHashMap, adding the maxDataMaps property and overriding the removeEldestEntry() method, as explained earlier. In addition, DataMapRepository contains a constructor that allows you to create copies of the repository instances, which can be useful if you want to take a snapshot of the repository while the application is running. Both the original repository and its copy will contain the same data map objects, which is correct because data maps cannot be modified, as you'll see later in this article.


Listing 7. The data repository class
                
package autosave;
...
import java.util.LinkedHashMap;
import java.util.Map;

public class DataMapRepository
        extends LinkedHashMap<String, Map<String, Object>> {
    private static final int DEFAULT_MAX_DATA_MAPS = 1000;
    private int maxDataMaps;

    public DataMapRepository() {
        maxDataMaps = DEFAULT_MAX_DATA_MAPS;
    }
    
    public DataMapRepository(DataMapRepository repository) {
        maxDataMaps = repository.maxDataMaps;
        putAll(repository);
    }
    
    public int getMaxDataMaps() {
        return maxDataMaps;
    }

    public void setMaxDataMaps(int maxDataMaps) {
        this.maxDataMaps = maxDataMaps;
    }

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > maxDataMaps;
    }
    ...
}

The getDataMap() and setDataMap() methods (see Listing 8) can be used to access the data maps of the repository. These two methods use getDataMapId() to generate the ID for the given context, then they call the get(), put(), and remove() methods inherited from the LinkedHashMap class.


Listing 8. Methods for storing and retrieving data maps
                
package autosave;
...
public class DataMapRepository ... {
    ...
    public Map<String, Object> getDataMap(FacesContext ctx) {
        String id = getDataMapId(ctx);
        if (id == null)
            return null;
        return get(id);
    }
    
    public void setDataMap(FacesContext ctx, Map<String, Object> dataMap) {
        String id = getDataMapId(ctx);
        if (id == null)
            return;
        if (dataMap != null)
            put(id, dataMap);
        else
            remove(id);
    }
    ...
}


Working in a multithreaded environment

The DataMapRepository class presented previously and its base class LinkedHashMap are not thread-safe. One way to deal with this is to use the synchronizedMap() method of java.util.Collections, which returns a thread-safe wrapper map. In the case of the form data repository, it is better to build a custom wrapper class, which controls how the repository is accessed, in addition to ensuring that it is safely used in a multithreaded server environment.

Using a thread-safe wrapper for the data repository

Listing 9 shows the wrapper class, which is named RepositoryWrapper in the sample application. It has a single field named repository of type DataMapRepository. The getRepository() method returns a copy of the private repository, and setRepository() creates a new copy, as well. These copies, however, contain the same data map objects as the original repository, which is OK because data maps are not modified after being created and populated with the form data.


Listing 9. The wrapper class of the data repository
                
package autosave;
...
public class RepositoryWrapper implements java.io.Serializable {
    private DataMapRepository repository;
    
    public RepositoryWrapper() {
        repository = new DataMapRepository();
    }
    
    public synchronized DataMapRepository getRepository() {
        return new DataMapRepository(repository);
    }

    public synchronized void setRepository(
            DataMapRepository repository) {
        if (repository != null)
            this.repository = new DataMapRepository(repository);
        else
            this.repository.clear();
    }
    ...
}

The RepositoryWrapper class contains thread-safe methods for accessing the data maps and the maxDataMaps property of the wrapped repository (see Listing 10). The getDataMap() and setDataMap() methods retrieve and store the data map for the given FacesContext. The hasDataMap() method returns true if a data map exists for the ctx parameter and the clearDataMap() method removes a data map from the repository.


Listing 10. Thread-safe methods for accessing the data repository
                
package autosave;
...
import javax.faces.context.FacesContext;
...
import java.util.Map;

public class RepositoryWrapper implements java.io.Serializable {
    ...
    public synchronized Map<String, Object> getDataMap(
            FacesContext ctx) {
        return repository.getDataMap(ctx);
    }
    
    public synchronized void setDataMap(FacesContext ctx,
            Map<String, Object> dataMap) {
        repository.setDataMap(ctx, dataMap);
    }
    
    public synchronized boolean hasDataMap(FacesContext ctx) {
        return getDataMap(ctx) != null;
    }
    
    public synchronized void clearDataMap(FacesContext ctx) {
        setDataMap(ctx, null);
    }
    
    public synchronized int getMaxDataMaps() {
        return repository.getMaxDataMaps();
    }

    public synchronized void setMaxDataMaps(int maxDataMaps) {
        repository.setMaxDataMaps(maxDataMaps);
    }
    ...
}

Configuring the wrapper as a JSF managed bean

All map objects containing the form data are kept in a repository instance whose wrapper is configured as a JSF managed bean in the faces-config.xml file (see Listing 11). The specified bean name is repositoryWrapper and the bean scope is application. The JSF configuration file can also be used to provide a value for the maxDataMaps property of the data repository.


Listing 11. Configuring the repository wrapper as a JSF managed bean
                
<faces-config>
    ...
    <managed-bean>
        <managed-bean-name>repositoryWrapper</managed-bean-name>
        <managed-bean-class>autosave.RepositoryWrapper</managed-bean-class>
        <managed-bean-scope>application</managed-bean-scope>
        ...
        <managed-property>
            <property-name>maxDataMaps</property-name>
            <value>100</value>
        </managed-property>
    </managed-bean>
    ...
</faces-config>

The RepositoryWrapper class has two static methods that return the managed bean instance. These methods (shown in Listing 12) can use a FacesContext or a ServletContext for retrieving the wrapper bean from the application scope.


Listing 12. Getting the managed bean instance
                
package autosave;

import javax.faces.application.Application;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;

import javax.servlet.ServletContext;
...
public class RepositoryWrapper implements java.io.Serializable {
    ...
    public static RepositoryWrapper getManagedBean(FacesContext ctx) {
        Application app = ctx.getApplication();
        ValueBinding vb = app.createValueBinding("#{repositoryWrapper}");
        return (RepositoryWrapper) vb.getValue(ctx);
    }

    public static RepositoryWrapper getManagedBean(ServletContext ctx) {
        return (RepositoryWrapper) ctx.getAttribute("repositoryWrapper");
    }

}


Modifying the JSF phase listener

In Part 1 of this series, you found a class named AutoSaveListener that was used to handle Ajax requests, retrieving the submitted data from the JSF component tree. In this article, the form data will be stored in the data repository, instead of being printed.

Saving the current view's data into the repository

Listing 13 shows the saveCurrentView() method of AutoSaveListener, which stores the form data of the current JSF view into the repository. The first step is to call getCurrentInstance(), which returns the faces context, then use getViewRoot() to obtain the root component of the JSF view. After that, saveCurrentView() creates a new data map and calls the saveValues() method of the DataMapRepository class to store the values of the JSF components into the data map. As a safety measure, the map object is passed to Collections.unmodifiableMap(), which returns a wrapper map that throws an exception at any attempt to modify its state. The unmodifiable map is stored into the repository with the setDataMap() method of the wrapper object.


Listing 13. Storing the form data of the current JSF view into the repository
                
package autosave;
...
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
...
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class AutoSaveListener implements PhaseListener {
    ...
    public void saveCurrentView() {
        // Get the faces context of the current request.
        FacesContext ctx = FacesContext.getCurrentInstance();
        // Get the root component of the current view.
        UIViewRoot root = ctx.getViewRoot();
        // Create a new data map.
        Map<String, Object> dataMap = new HashMap<String, Object>();
        // Store the component values into the data map.
        DataMapRepository.saveValues(root, dataMap);
        // Make the data map unmodifiable.
        dataMap = Collections.unmodifiableMap(dataMap);
        // Get the managed bean instance wrapping the data repository.
        RepositoryWrapper wrapper = RepositoryWrapper.getManagedBean(ctx);
        // Store the data map into the repository.
        wrapper.setDataMap(ctx, dataMap);
        // Stop request processing.
        ctx.responseComplete();
    }
    ...
}

As you probably remember from Part 1, the form auto-saving should not interfere with the application logic. The data model must not be updated and no action method should be called after an auto-save. After saving the values of the input components, saveCurrentView() must stop the request processing so that the auto-saved data, which is actually the user input of a partially filled form, isn't stored into the application data model. Therefore, saveCurrentView() calls the responseComplete() method of the faces context to signal that the JSF request processing life cycle should be interrupted.

Handling the JSF phase event

As explained in Part 1, the form data must be saved after the JSF validation phase, when all values of the JSF components have been converted and validated by the JSF framework. In addition to verifying the phase ID of the PhaseEvent parameter, the afterPhase() method (shown in Listing 14) gets the Ajax-Request header to see if the current request auto-saves the form data. This header is set in the submitFormData() function of the AutoSaveScript.js file, which was presented in Part 1. If the header's value is Auto-Save, the afterPhase() method calls saveCurrentView() to store the valid values of the JSF components into the data repository.

If the header marking the Ajax requests is not present, the user must have clicked the Submit button of the Web form. In this case, afterPhase() verifies if the faces context contains any messages, whose absence indicates that the user-submitted data is valid because afterPhase() is invoked after the JSF validation phase. If the form data is valid, any previously saved data for the current user and view combination is cleared from the repository with clearDataMap().


Listing 14. Listening for JSF phase events
                
package autosave;
...
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
...
public class AutoSaveListener implements PhaseListener {
    ...
    public void afterPhase(PhaseEvent e) {
        if (!e.getPhaseId().equals(PhaseId.PROCESS_VALIDATIONS))
            return;
        FacesContext ctx = e.getFacesContext();
        Map headers = ctx.getExternalContext().getRequestHeaderMap();
        if ("Auto-Save".equals(headers.get("Ajax-Request"))) {
            // Auto-Save Request. Save data into the repository.
            saveCurrentView();
        } else {
            // The user must have clicked the Submit button.
            if (!ctx.getMessages().hasNext()) {
                // There are no error messages.
                // This means the final submitted data is valid and
                // the temporary auto-saved data is no longer needed.
                RepositoryWrapper wrapper
                    = RepositoryWrapper.getManagedBean(ctx);
                wrapper.clearDataMap(ctx);
            }
        }
    }
    ...
}


Implementing the repository's persistence

In this section, the sample application is enhanced to save the repository on the server before shutdown and restore it when the server starts again. Object serialization is an easy way for implementing the repository's persistence since the DataMapRepository class, the contained data maps, and their elements are serializable. Remember that data maps contain the values of the JSF components, which must be serializable according to the JSF specification.

Object serialization has its well-known flaws, but in this case, it is a reasonable persistence solution because the number of form data instances that are kept in the repository is limited. Also, there is no tragedy if the temporary data of the repository is lost in the event that the server crashes due to some problem unrelated to the application. A more reliable solution, based on a relational or object database, would require more CPU resources, whose allocation can't be justified for storing the partial user input, which is updated every 10 seconds. Nevertheless, the repository's state should survive a server or application restart.

Using a ServletContextListener

The DataMapPersistence class of the sample application implements the javax.servlet.ServletContextListener interface so it can get notifications when the application is started and stopped. The listener methods serialize and deserialize the data repository , using the File returned by the getDataFile() method (shown in Listing 15). The data file is kept in the WEB-INF directory of the sample application.


Listing 15. Getting the file of the data repository
                
package autosave;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextListener;
...
import java.io.File;

public class DataMapPersistence implements ServletContextListener {

    private File getDataFile(ServletContext sctx) {
        String path = sctx.getRealPath("/WEB-INF/repository.ser");
        if (path == null)
            return null;
        return new File(path);
    }
    ...
}

Loading the data repository

The servlet/JSP container will call the contextInitialized() method during the application initialization. Listing 16 shows this method, which deserializes the repository object and sets a context attribute named loadedRepository, which can be accessed later, using the JSP/JSF EL. The servlet context's attributes are the same thing as the JSP/JSF variables kept in the application scope. The loaded repository will be used later to set the repository property of the RepositoryWrapper bean.


Listing 16. Loading the data repository during application initialization
                
package autosave;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
...
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DataMapPersistence implements ServletContextListener {
    ...    
    public void contextInitialized(ServletContextEvent e) {
        ServletContext sctx = e.getServletContext();
        File dataFile = getDataFile(sctx);
        if (dataFile == null || !dataFile.exists())
            return;
        try {
            ObjectInputStream in = new ObjectInputStream(
                    new BufferedInputStream(
                    new FileInputStream(dataFile)));
            try {
                // Read the data repository from the file.
                Object repository = in.readObject();
                // Store the loaded repository into the application scope.
                sctx.setAttribute("loadedRepository", repository);
            } finally {
                in.close();
            }
        } catch (Exception x) {
            sctx.log("Loading Error", x);
        }
    }
    ...
}

No servlet or JSP page will be invoked before the contextInitialized() call, which means the JSF framework might not be initialized yet. Therefore, contextInitialized() cannot set the repository property of the RepositoryWrapper bean, which is managed by the JSF framework. This setting is done in the faces-config.xml file, where the loadedRepository variable from the application scope can be safely used to set the repository property of the managed bean (see Listing 17).


Listing 17. Storing the loaded data into the repository wrapper
                
<faces-config>
    ...
    <managed-bean>
        <managed-bean-name>repositoryWrapper</managed-bean-name>
        <managed-bean-class>autosave.RepositoryWrapper</managed-bean-class>
        <managed-bean-scope>application</managed-bean-scope>
        <managed-property>
            <property-name>repository</property-name>
            <value>#{loadedRepository}</value>
        </managed-property>
        ...
    </managed-bean>
    ...
</faces-config>

Saving the data repository

The servlet/JSP container will call the contextDestroyed() method when the application is shut down. After getting the RepositoryWrapper instance that is managed by the JSF framework, the contextDestroyed() method retrieves a copy of the data repository from the wrapper bean and serializes the repository's copy into the data file (see Listing 18).


Listing 18. Saving the repository's data in a file before application shutdown
                
package autosave;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
...
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class DataMapPersistence implements ServletContextListener {
    ...    
    public void contextDestroyed(ServletContextEvent e) {
        ServletContext sctx = e.getServletContext();
        File dataFile = getDataFile(sctx);
        if (dataFile == null)
            return;
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new BufferedOutputStream(
                    new FileOutputStream(dataFile)));
            try {
                // Get a copy of the data repository from the wrapper bean.
                RepositoryWrapper wrapper
                    = RepositoryWrapper.getManagedBean(sctx);
                Object repository = wrapper.getRepository();
                // Serialize the data repository into the file.
                out.writeObject(repository);
            } finally {
                out.close();
            }
        } catch (Exception x) {
            sctx.log("Saving Error", x);
        }
    }

}
            

Listing 19 shows how the DataMapPersistence class is configured as a servlet context listener in the web.xml file.


Listing 19. Configuring the servlet context listener
                
<web-app ...>
    ...
    <listener>
        <listener-class>autosave.DataMapPersistence</listener-class>
    </listener>
    ...
</web-app>
            

Conclusion

In this second part of the series, you've learned how to identify users across sessions, how to implement a thread-safe repository for the form data, how to save and restore the values of the JSF input components, and how to implement the persistence for the data repository. In the third and final part of the series, you'll see how to fill out JSF forms with the saved data and how to use more JavaScript techniques in JSF applications.



Download

DescriptionNameSizeDownload method
Sample applicationwa-aj-jsf2.zip19KB HTTP

Information about download methods


Resources

Learn

  • Find more Ajax resources on Wikipedia.

  • Find useful resources related to Java Maps at The Collections Framework.

  • The Java Servlet Specification provides more information on servlet listeners and filters.

  • Find everything you need know about Forms in HTML 4.01 Documents.

  • The developerWorks Web development zone is packed with tools and information for Web 2.0 development.

  • The developerWorks Ajax resource center contains a growing library of Ajax content as well as useful resources to get you started developing Ajax applications today.

  • Be sure to check out Part 1 of this series, which lays the foundation for the tasks described in this article. And, stay tuned for Part 3 of this series, where you'll wrap up the process.

Get products and technologies

Discuss

About the author

Andrei Cioroianu is a Senior Java Developer and Consultant at Devsphere, a provider of custom Java EE development and Ajax/JSF consulting services. You can reach Andrei through the contact form at www.devsphere.com.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Java technology
ArticleID=254834
ArticleTitle=Auto-save JSF forms with Ajax: Part 2
publish-date=09182007
author1-email=andcio@gmail.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers