Previous Section  < Day Day Up >  Next Section

5.2 Implementing the Business Logic Classes

The expense report application contains three business logic classes, shown in Figure 5-2. These classes have no dependencies on JSF (or any other presentation technology, for that matter).

Figure 5-2. The business logic classes
figs/Jsf_0502.gif

A Report contains ReportEntry instances, and Report instances are saved in a ReportRegistry, which is an abstract class with concrete subclasses for different storage medias. The FileReportRegistry implements a simple filesystem-based storage facility.

5.2.1 The ReportEntry Class

The com.mycompany.expense.ReportEntry class, shown in Example 5-1, is a simple bean, with properties for all expense report entry items—the date, the expense type, and the amount—plus the entry's ID, unique within a Report.

Example 5-1. The ReportEntry class
package com.mycompany.expense;



import java.io.Serializable;

import java.util.Date;



public class ReportEntry implements Serializable {

    private int id = -1;

    private Date date;

    private int type;

    private double amount;



    public ReportEntry( ) {

    }



    public ReportEntry(ReportEntry src) {

        this.setId(src.getId( ));

        this.setDate(src.getDate( ));

        this.setType(src.getType( ));

        this.setAmount(src.getAmount( ));

    }



    public int getId( ) {

        return id;

    }



    public void setId(int id) {

        this.id = id;

    }



    public Date getDate( ) {

        if (date == null) {

            date = new Date( );

        }

        return date;

    }



    public void setDate(Date date) {

        this.date = date;

    }



    public int getType( ) {

        return type;

    }



    public void setType(int type) {

        this.type = type;

    }



    public double getAmount( ) {

        return amount;

    }



    public void setAmount(double amount) {

        this.amount = amount;

    }



    public String toString( ) {

        return "id: " + id + " date: " + date + " type: " + type + 

            " amount: " + amount;

    }

}

Each property is represented by standard JavaBeans accessor methods: getId() and setId(), getDate() and setDate(), getType() and setType(), and getAmount() and setAmount(). The ReportEntry class also has a copy constructor, i.e., a constructor that initializes the new instance's properties to the values found in the instance provided as an argument. I'll return to how this constructor is used shortly when we look at the Report class.

In addition to the property accessor methods and the copy-constructor, the ReportEntry class implements the Serializable interface, so that the FileReportRegistry can save instances of this class to a file.

While implementing the java.io.Serializable interface is all it takes to make a class serializable, it's not something to be done without careful consideration. Serialization comes with potential maintenance and security issues that need to be dealt with, e.g., by implementing the readObject() and writeObject( ) methods and declaring a static serialVersionUID variable. The classes in this book are intended only as basic examples to illustrate how to use JSF, and are serializable only so that I can use a simple filesystem-based permanent storage. I therefore ignore dealing with versioning issues and other details. I recommend reading Joshua Bloch's Effective Java (Addison-Wesley) if you want to learn more about robust serialization strategies.


The toString() method in the ReportEntry class returns a String with all the property values, which is handy when writing log entries, e.g., for debugging.

5.2.2 The Report Class

Example 5-2 shows part of the com.mycompany.expense.Report class.

Example 5-2. The Report class variables and constructors
package com.mycompany.expense;



import java.io.Serializable;

import java.util.ArrayList;

import java.util.Collections;

import java.util.Comparator;

import java.util.Date;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Map;



public class Report implements Serializable {

    public static final int STATUS_NEW = 0;

    public static final int STATUS_OPEN = 1;

    public static final int STATUS_SUBMITTED = 2;

    public static final int STATUS_ACCEPTED = 3;

    public static final int STATUS_REJECTED = 4;



    private int currentEntryId;

    private int id = -1;

    private String title;

    private String owner;

    private int status = STATUS_NEW;

    private Map entries;



    public Report( ) {

        entries = new HashMap( );

    }



    public Report(Report src) {

        setId(src.getId( ));

        setTitle(src.getTitle( ));

        setOwner(src.getOwner( ));

        setStatus(src.getStatus( ));

        setEntries(src.copyEntries( ));

        setCurrentEntryId(src.getCurrentEntryId( ));

    }



    public synchronized int getId( ) {

        return id;

    }



    public synchronized void setId(int id) {

        this.id = id;

    }



    public synchronized String getTitle( ) {

        return title;

    }



    public synchronized void setTitle(String title) {

        this.title = title;

    }



    public synchronized String getOwner( ) {

        return owner;

    }



    public synchronized void setOwner(String owner) {

        this.owner = owner;

    }



    public synchronized int getStatus( ) {

        return status;

    }



    public synchronized void setStatus(int status) {

        this.status = status;

    }

Just like ReportEntry, the Report class implements Serializable to make it easy to save reports to a file. Also like ReportEntry, it's a bean with a number of properties: id, title, owner, and status. The status is represented as an int value defined by static final variables: STATUS_NEW, STATUS_OPEN, STATUS_ACCEPTED, and STATUS_REJECTED. A java.util.Map holds the report entries, with the report entry IDs as keys and the ReportEntry instances as values. The Report class assigns an ID to each report entry when it's added to the report, and it uses an int variable to keep track of the next available ID.

The Report class has a copy constructor that initializes the new instance with the values from another Report instance by calling the property accessor methods plus four private methods: setEntries(), copyEntries(), setCurrentEntry( ), and getCurrentEntry(). These private methods are shown in Example 5-3.

Example 5-3. Private initialization methods for the Report class
    private int getCurrentEntryId( ) {

        return currentEntryId;

    }



    private void setCurrentEntryId(int currentEntryId) {

        this.currentEntryId = currentEntryId;

    }



    private Map copyEntries( ) {

        Map copy = new HashMap( );

        Iterator i = entries.entrySet( ).iterator( );

        while (i.hasNext( )) {

            Map.Entry e = (Map.Entry) i.next( );

            copy.put(e.getKey( ), new ReportEntry((ReportEntry) e.getValue( )));

        }

        return copy;

    }



    private void setEntries(Map entries) {

        this.entries = entries;

    }

}

The getCurrentEntryId() and setCurrentEntryId() methods get and set the variable holding the next available report entry ID.

The copyEntries() method loops through all report entries and returns a new Map with copies of all ReportEntry instances. This is where the ReportEntry copy constructor first comes into play. The setEntries() method simply saves a reference to the provided Map as the new entries list.

All four methods are private because no one should ever mess with these values directly; these methods are to be used only by the Report copy constructor. A set of other methods provides public access to the report entries instead. Example 5-4 shows the addEntry() method.

Example 5-4. Adding a report entry to the Report
    public synchronized void addEntry(ReportEntry entry) {

        entry.setId(currentEntryId++);

        entries.put(new Integer(entry.getId( )), new ReportEntry(entry));

    }

First, note that this method is synchronized. This is true for all methods accessing report content, in order to ensure that multiple threads can access the report at the same time without corrupting its state. Multithreading issues and thread-safety strategies are out of scope for this book, but it's a very important consideration when developing server-side applications. For a web application, all objects held in the application scope must be handled in a threadsafe manner; because all users and all requests have access to the objects in this scope, it's very likely that more than one thread will access the same object. Objects in the session scope must also be threadsafe, because they are shared by all requests from the same user. If the user makes parallel requests, e.g., by submitting the same form over and over without waiting for the response, making requests from multiple browsers tied to the same session, or requesting pages that contain references (e.g., frame references) to other pages that modify session scope objects, a session scope object's state can be corrupted. To learn more about thread-safety strategies, I recommend that you read Java Threads by Scott Oaks and Henry Wong (O'Reilly) or another book that deals exclusively with this subject. Joshua Bloch's Effective Java (Addison-Wesley) is another good source for tips about thread safety and a lot of other topics of importance to most large-scale Java projects.

The addEntry() method first sets the ID of the ReportEntry instance argument to the next available ID and increments the ID counter in preparation for the next time the method is called. It then adds a copy of the ReportEntry instance to its entries Map. Saving a copy of the ReportEntry instance is important, because it's the only way to guarantee that what's in the report isn't accidentally modified if another part of the application makes changes to the argument instance later. Imagine what would happen if an employee, still holding on to the instance used as the argument, could change the amount of an entry in a report the manager has already approved.

The methods for removing an entry, getting a specific entry, and getting all entries are shown in Example 5-5.

Example 5-5. Removing and retrieving entries
    public synchronized void removeEntry(int id) {

        entries.remove(new Integer(id));

    }



    public synchronized ReportEntry getEntry(int id) {

        return new ReportEntry((ReportEntry) entries.get(new Integer(id)));

    }



    public synchronized List getEntries( ) {

        return new ArrayList(copyEntries( ).values( ));

    }

The removeEntry() method simply removes the entry with a matching ID from the report's Map. The getEntry() and getEntries() methods return a copy of a single ReportEntry and a java.util.List with copies of all ReportEntry instances, respectively.

Example 5-6 shows the remaining methods in the Report class.

Example 5-6. Entries-based property accessor methods
    public synchronized Date getStartDate( ) {

        Date date = null;

        if (!entries.isEmpty( )) {

            List l = getEntriesSortedByDate( );

            date = ((ReportEntry) l.get(0)).getDate( );

        }

        return date;

    }

        

    public synchronized Date getEndDate( ) {

        Date date = null;

        if (!entries.isEmpty( )) {

            List l = getEntriesSortedByDate( );

            date = ((ReportEntry) l.get(entries.size( ) - 1)).getDate( );

        }

        return date;

    }

        

    public synchronized double getTotal( ) {

        double total = 0;

        Iterator i = entries.values( ).iterator( );

        while (i.hasNext( )) {

            ReportEntry e = (ReportEntry) i.next( );

            total += e.getAmount( );

        }

        return total;

    }



    public String toString( ) {

        return "id: " + id + " title: " + getTitle( ) + 

            " owner: " + getOwner( ) + 

            " startDate: " + getStartDate( ) + " endDate: " + getEndDate( ) + 

            " status: " + getStatus( );

    }

    private List getEntriesSortedByDate( ) {

        List l = getEntries( );

        Collections.sort(l, new Comparator( ) {

                public int compare(Object o1, Object o2) {

                    Date d1 = ((ReportEntry) o1).getDate( );

                    Date d2 = ((ReportEntry) o2).getDate( );

                    return d1.compareTo(d2);

                }

        });

        return l;

    }

The first three public methods are property accessors for read-only properties based on the list of report entries. getStartDate() gets a list of all entries sorted by the date property from the getEntriesSortedByDate() method and returns the date property value for the first one. The getEndDate() method is similar; it gets the sorted entries list and returns the date property value for the last entry in the list. getTotal() loops through all entries and returns the sum of their amount property values.

The private getEntriesSortedByDate() method uses a java.util.Comparator that compares ReportEntry instances by their date property values, combined with the java.util.Collections sort() method to sort the entries.

Finally, the toString() method is just a handy method for debugging, returning a String with the property values.

5.2.3 The ReportRegistry and FileReportRegistry Classes

ReportRegistry is an abstract class that defines a generic interface for maintaining a list of expense reports, shown in Example 5-7.

Example 5-7. The ReportRegistry class
package com.mycompany.expense;



import java.util.Date;

import java.util.List;



public abstract class ReportRegistry {

    public abstract void addReport(Report report) throws RegistryException;



    public abstract void updateReport(Report report) throws RegistryException;



    public abstract void removeReport(Report report) throws RegistryException;



    public abstract Report getReport(int id) throws RegistryException;



    public abstract List getReports(String owner, Date from, Date to,

                                    int[] status) throws RegistryException;

}

It defines methods for adding, updating, removing, and getting a single Report, and one method for getting a set of Report instances that matches a search criteria.

The concrete subclass of the ReportRegistry used in this book is called FileReportRegistry. It's a simple implementation that uses a file in the user's home directory for persistence. The constructor initializes the registry from the file, and all methods that modify the registry also save the updated registry to the file. This is okay as a proof-of-concept, but it would be too slow for a registry with many reports. In a real application, I would use a subclass that keeps the registry information in a database instead. Example 5-8 shows the instance variables and the constructor for the FileReportRegistry class.

Example 5-8. The FileReportRegistry variables and constructor
package com.mycompany.expense;



import java.io.File;

import java.io.FileNotFoundException;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.util.ArrayList;

import java.util.Collections;

import java.util.Date;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Map;



public class FileReportRegistry extends ReportRegistry {

    private int currentReportId;

    private Map reports;



    public FileReportRegistry( ) throws RegistryException {

        reports = new HashMap( );

        try {

            load( );

        }

        catch (IOException e) {

            throw new RegistryException("Can't load ReportRegistry", e);

        }

    }

    ...

The FileReportRegistry is similar to the Report class, except that it maintains a set of reports instead of report entries. It keeps the Report instances in a java.util.Map variable, with the report IDs as keys and the Report instances as values. It assigns an ID to a report when it's added to the registry and uses an int variable to keep track of the next available ID.

The constructor calls the load() method, shown in Example 5-9.

Example 5-9. Loading the FileReportRegistry information
    ...

    private void load( ) throws IOException {

        File store = getStore( );

        try {

            ObjectInputStream is = 

                new ObjectInputStream(new FileInputStream(store));

            currentReportId = is.readInt( );

            reports = (Map) is.readObject( );

        }

        catch (FileNotFoundException fnfe) {

            // Ignore

        }

        catch (ClassNotFoundException cnfe) {

            // Shouldn't happen, but log it if it does

            System.err.println("Error loading ReportRegistry: " +

                               cnfe.getMessage( ));

        }

    }

The load() method gets a java.io.File for the persistence file by calling the getStore() method and opens a java.io.ObjectInputStream for the file. It then initializes the next available report ID and the Map containing all entries with data from the file. It catches the java.io.FileNotFoundException and ignores it, so that things work fine even if no file has been created yet, but it lets all other types of java.io.IOException subtypes through to signal problems, such as insufficient file permissions or corrupt data.

The save() method, shown in Example 5-10, is called every time a report is added, updated, or removed from the registry.

Example 5-10. Saving the FileReportRegistry information
    private void save( ) throws IOException {

        File store = getStore( );

        ObjectOutputStream os = 

            new ObjectOutputStream(new FileOutputStream(store));

        os.writeInt(currentReportId);

        os.writeObject(reports);

    }

The save() method is the reverse of the load() method: it gets the persistence file, opens an ObjectOutputStream() for it, and writes the current report ID index and the entries Map. It also throws a potential IOException back at the caller.

Example 5-11 shows the getStore() method.

Example 5-11. Creating a File instance for the persistence file
    private File getStore( ) {

        File store = null;

        File homeDir = new File(System.getProperty("user.home"));

        File persistenceDir = new File(homeDir, ".expense");

        if (!persistenceDir.exists( )) {

            persistenceDir.mkdir( );

        }

        return new File(persistenceDir, "store.ser");

    }

The persistence file is named store.ser, located in a subdirectory named .expense in the home directory for the account running the application. The getStore() method first creates a File instance for the subdirectory, and if the directory doesn't exist in the filesystem, the method creates the directory. getStore() then creates a File instance for the persistence file and returns it.

Let's look at the methods that deal with the registry content next, starting with the addReport( ) method shown in Example 5-12.

Example 5-12. Adding a report to the FileReportRegistry
    public synchronized void addReport(Report report)

        throws RegistryException{

        report.setId(currentReportId++);

        reports.put(new Integer(report.getId( )), new Report(report));

        try {

            save( );

        }

        catch (IOException e) {

            throw new RegistryException("Can't save ReportRegistry", e);

        }

    }

Just as with the Report class, the addReport() method and all other methods that access registry content are synchronized to allow multiple threads to call them without corrupting the registry state.

The addReport() method first sets the ID of the Report instance argument to the next available ID and increments the ID counter. It then adds a copy of the Report instance to its reports Map, to prevent later changes to the argument object from affecting the state of the registry. Finally, the addReport() method saves the updated registry information by calling save().

The updateReport() and removeReport( ) methods shown in Example 5-13 follow a similar pattern.

Example 5-13. Updating and removing a report to the FileReportRegistry
    public synchronized void updateReport(Report report)

        throws RegistryException{

        checkExists(report);

        reports.put(new Integer(report.getId( )), new Report(report));

        try {

            save( );

        }

        catch (IOException e) {

            throw new RegistryException("Can't save ReportRegistry", e);

        }

    }



    public synchronized void removeReport(Report report)

        throws RegistryException{

        checkExists(report);

        reports.remove(new Integer(report.getId( )));

        try {

            save( );

        }

        catch (IOException e) {

            throw new RegistryException("Can't save ReportRegistry", e);

        }

    }

    ...

    private void checkExists(Report report) {

        Integer id = new Integer(report.getId( ));

        if (reports == null || reports.get(id) == null) {

            throw new IllegalStateException("Report " + report.getId( ) +

                                            " doesn't exist");

        }

    }

The updateReport() and removeReport( ) methods call checkExists() to verify that the provided Report instance corresponds to an existing report in the registry. If it doesn't, checkExists() throws an IllegalStateException. The updateReport( ) method then replaces the Report instance that matches the argument with a copy of the argument instance, while removeReport() simply removes the matching report.

Example 5-14 shows the getReport( ) method.

Example 5-14. Getting a report from the FileReportRegistry
    public synchronized Report getReport(int id) {

        return (Report) reports.get(new Integer(id));

    }

No surprises here: getReport() simply returns a copy of the Report with an ID matching the specified one, or null if it doesn't exist.

The getReports() method shown in Example 5-15 is more exciting.

Example 5-15. Getting a set of reports from the FileReportRegistry
    public synchronized List getReports(String owner, Date fromDate,

                                        Date toDate, int[] status) {

        List matches = new ArrayList( );

        Iterator i = reports.values( ).iterator( );

        while (i.hasNext( )) {

            Report report = (Report) i.next( );

            if (matchesCriteria(report, owner, fromDate, toDate, status)) {

                matches.add(new Report(report));

            }

        }

        return matches;

    }



    private boolean matchesCriteria(Report report, String owner, 

                                    Date from, Date to, int[] status) {

        boolean matches = false;

        if ((owner == null || owner.equals(report.getOwner( ))) &&

            (from == null || (report.getStartDate( ) != null &&

             report.getStartDate( ).getTime( ) >= from.getTime( ))) &&

            (to == null || (report.getStartDate( ) != null &&

             report.getStartDate( ).getTime( ) <= to.getTime( )))) {

            if (status == null) {

                matches = true;

            }

            else {

                for (int i = 0; i < status.length; i++) {

                    if (report.getStatus( ) == status[i]) {

                        matches = true;

                        break;

                    }

                }

            }

        }

        return matches;

    }

The getReports() method loops through all reports and calls matchesCriteria() for each one. If the report matches the criteria, the method adds a copy of the Report instance to the List it returns after checking all reports.

The matchesCritieria() method compares all criteria arguments that have non-null values with the corresponding properties of the Report argument and returns true if they all match. The owner criterion matches reports only with the specified owner; the date range criterion matches reports with start dates that fall within the range; and the status criterion matches reports in one of the listed statuses.

    Previous Section  < Day Day Up >  Next Section