Building a Java servlet framework using reflection, Part 2

Gain greater functionality with less code using these reflective code samples

1 2 3 Page 2
Page 2 of 3
        // Load the rules table from the business rules object
        // this method may be moved to a utility class later!
        hRulesTable = loadBusinessRules(sActionName);
        // Retrieve the keys from the businessData hashtable passed 
            // into the isValid method.
        Enumeration enumKeys = hBusinessData.keys();
        // Initialize the rule object
        Rule rule = null;
        // While there are still keys remaining in the business data
        while (enumKeys.hasMoreElements() ) {
            // Store the key name
            String fieldName = (String) enumKeys.nextElement();
            // Store the user-entered value
            String userValue = (String) hBusinessData.get( fieldName);
            // Retrieve the rule from the rules table
            Rule rule = (Rule) hRulesTable.get(fieldName);
            // the field is in the mandatory hash table
            if ( rule != null) {    
                  // the field is mandatory, either because it is required
                // retrieve rules
                String fieldType = rule.getValidation();
                boolean bFieldIsRequired = rule.isMandatory();
                        // trim the extra spaces from the field
                userValue = userValue.trim();
                // check if the field is blank
                boolean fieldNotBlank = (userValue.length() > 0);
                if ( fieldNotBlank) {
                    // if not blank, perform field-level validation
                    isFieldValid = 
                                validateField( fieldName, fieldType, userValue);
                } else if ( fieldIsRequired)    {
                    // if the field is blank, it may or may 
                              // not be valid, depending on
                    // whether or not it is required.
                    // If the field is required, it is not valid
                    // If the field is not required, it is valid
                    isFieldValid = false;
                }
                // If the field is not valid, a missing tag will appear
                if ( !isFieldValid) {
                    setError( fieldName, businessData);
                }
            } else { // the field is not contained in the hashtable
                // If this is not a field we care about
                if ( ignoreField( fieldName) )
                {
                    // just ignore here
                } else {
                    // the value is missing from the table
                    // there's a BIG problem!!!
                    throw new DataProcessingException( 
                                 "missing key value for key " 
                                 + fieldName 
                                 + " in rules table" );
                }
            }
            // if there's an error, add the error to the businessData
            // The return value is the previous value AND the current
                  // field-level value
            returnValue = returnValue && isFieldValid;
            // reset the field-level validation for the next iteration
            isFieldValid = true;
        }  
        return returnValue;
    }

That's a lot of code, but it is completely independent of the individual forms it validates. Let's step through it quickly. Initially, the business rules corresponding to the form are loaded using the previously described loadBusinessRules method. Having loaded the Hashtable containing the rules, you retrieve the keys corresponding to the business data that the form supplies. You'll loop over all the keys in the Enumeration and retrieve the name-value pairs for validation. Once the name is retrieved, you can use it to find the corresponding rule in the Hashtable. If you find the rule in the Hashtable, you can determine whether it is blank, or required. If the field is not blank, the validateField method is called to perform validation.

The validateField method also uses reflection to load the validation object associated with the field being validated. For example, the email field has an EmailValidation class. This class would be responsible for using some basic rules to determine whether the field entered resembles an email address -- if the at symbol (@) is present, for instance. I have not elaborated on this validation in this article because it is fairly self-explanatory.

This validation framework reduces the interdependencies of the teams developing the business objects and those developing the presentation layer. The development teams must agree upon a naming convention for all the fields used in the HTML in order to ensure proper application behavior. It is important that your development team agree on the field names early in order to proceed with development. As more actions are added to the system, you would only need to develop additional rules (which will likely be contained in property files anyway) and any additional validation rules, if necessary. In other words, little development would be necessary.

SUBHEAD_BREAK: Error handling

Perhaps the most important aspect of this servlet framework is its support for a unified error-handling scheme. With such a scheme, it is possible to build a sophisticated scheme to handle errors around the main proxy.

Development of error handling

As described above, the proxy servlet calls the business objects. The business objects are then initialized and begin performing their associated processes. The call to initialize the business object is present within a try-catch block that will catch the ExampleException. This exception will result in a call to the exception handler to perform the necessary processes associated with the exception.

For example, should a database exception occur, an email could be sent to the party responsible for database support. In fact, exceptions could be given classifications ranging from warning to critical, with behaviors attached to each category.

Let's assume that the database failure category is classified as critical. The DatabaseFailureException would extend the CriticalException, which would then extend the ExampleException. In this manner, the more specific behavior could build upon the generic error handling.

The following code samples show the progression from generic error handling to the specific handling of the database failure.

Public abstract class ExampleExceptionHandler 
{
    public abstract void handleException 
         throws ExampleException(ExampleException e);
}

The ExampleExceptionHandler class is an abstract class; it is used as a base exception handler that you can extend. This class may contain functionality, should your application require it.

public class CriticalExceptionHandler extends ExampleExceptionHandler {
    public void handleException throws ExampleException(CriticalException e)    {
        // Send email to the application support team
        sendMail("mcymerman", "error", e.getMessage() );
    }
    public void sendMail(String to, String subject, String message) {
        // normal commands to send email
        ...
    }
}

The critical exception handler by default sends an exception to the application support team to ensure that the application is monitored.

public class DatabaseFailureExceptionHandler extends CriticalExceptionHandler{
  public void handleException(DatabaseFailureException e)
    throws ExampleException
  {
    // send mail to the database support team
    sendMail("dba", "error", "fix the database please "+ e.getMessage() );
  }
}

Should the application encounter a database failure, the database failure exception handler will be instantiated, as I'll explain. The simple handler developed for this article will send email to the database support personnel to ensure that they are monitoring the application database.

Now that I've developed the simple error-handling framework, I'll explain how the exception handlers are instantiated and invoked. As I've mentioned earlier, the exception service will retrieve the class name of the specific exception that has been thrown using the getName() method of the Class object.

This method will return a string with the fully qualified name. The ExceptionService will use this name to dynamically load the exception handler associated with this exception, as follows:

try {
    ExceptionHandler exceptionHandler = loadExceptionHandler( exception);
    ...
} catch ( ClassNotFoundException cnfe){
    // load default exception handler
    ... 
}

The ExceptionHandler is loaded dynamically, based on the objects' naming convention. It's true that this scheme limits your naming choices; however, the resulting design is greatly simplified. The code in the try-catch block shown above uses a private method in the exception service to load the corresponding exception-handling code. This method can be developed as shown below:

private ExceptionHandler loadExceptionHandler( String sExceptionName, Exception exception)
{
      String HANDLER = "Handler"; // Should come from a properties file 
    String sClassName = null;
    if (sExceptionName == null && exception == null)    {
        // load the default exception handler and return
        ...
    }
      if ( sExceptionName == null) { 
      // the sExceptionName is passed in from the parent class
        Class classException = exception.getClass();
        sClassName = classException.getName();
    } else {
        
        sClassName = sExceptionName;        
      }
    // The string to load is the name of the exception plus the string
      // identifying it as a handler
      String sExceptionHandler =  classException.getName()+ HANDLER;
    ExceptionHandler ehExceptionHandler = null;
    try{
    // attempt to load the exception handler using the class.forName
      } catch (ClassNotFoundException exClassNotFound){
      // Get the name of the parent class of the exception
      sExceptionHandler = classException.getSuperClass().getName();
      if ( sExceptionHandler != DEFAULTEXCEPTIONHANDLER ){
        //  load the parent class exception handler           
          //  (this is a recursive method, 
          //  until the DefaultExceptionHandler is returned)
        loadExceptionHandler( sExceptionHandler, exception);
        }else{
        // load the default error handler
        }
      }
      return ehExceptionHandler;
}

This exception-handling framework lets you build up your exception handling to the extent necessary to achieve your required functionality. The exception handler will work its way up the exception handler hierarchy and load the most specific exception handler. Should it traverse the entire tree up to the DefaultExceptionHandler, it will load that handler. What have you gained through this framework? You now have the confidence that an exception handler will handle the exception. You also have the ability to handle specific errors, such as database failures, differently than presentation failures, through custom ExceptionHandlers. The processing required to load the different exception handling routines is handled through centralized code, thereby reducing the project's coding effort.

Data access

Articles about frameworks are often undermined by the numerous exceptions to every rule they put forward. In Part 1, I presented a simple logging interface for data access. It is difficult to present a framework that considers every case -- so I chose to show a simple data object that you can use to log a user's actions to the database. Should your application require a query for data presentation, you should use a different interface.

For this article, I'm considering a system in which a user's actions are logged in to the database, and the values entered on the forms are stored there. In order to gain access to the system, the user must be authenticated against the database. Therefore, the database interface must contain methods for running a query in addition those that govern to the logging and storage operations.

Developing data access

Here's the database interface I have chosen to perform the logging and query functionality:

public interface DataInterface
{
    // To connect and disconnect
    public void connect() throws DatabaseConnectivityException;
    public void disconnect() throws DatabaseConnectivityException;
    // For logging of user actions and performed transactions.
      public boolean createTransaction() throws DatabaseConnectivityException;
        public boolean beginTransaction() throws DatabaseConnectivityException;
    public boolean commitTransaction() throws DatabaseConnectivityException;
    public boolean rollbackTransaction() throws DatabaseConnectivityException;
    // For the logging and queries.
    public Vector execute(String sqlString) 
          throws DatabaseConnectivityException;
}

This interface is intended to satisfy the requirements for the example system traced throughout this article. It is necessary for the system to log user actions, process user requests, and run reports based on the values stored in the database. This interface could be further expanded should the requirements call for more detailed interaction with the database. It's important to note that you can load the database connection parameters using the action name as their key. In this way, the action name becomes the driving force behind this system.

As you've probably already noticed, the execute method returns a Vector object. This lets you use the execute method to run queries and perform inserts, updates, and deletes on the database as necessary. Why have I chosen to use a Vector instead of a JDBC ResultSet? I generally feel that the database-specific code should be contained in the classes closest to the database rather than classes removed from the SQL. There is more overhead involved in doing so, making this abstraction a prime candidate for optimization should your system performance slow down due to large queries.

Using a philosophy similar to that of the BusinessRules, you can use a DatabaseRules object to store the SQL needed to complete the transaction. In this case, I've chosen a vector to ensure that the SQL is performed in the order necessary to ensure relational integrity.

// initialize the vector to contain the data rules for the register user action
public static Vector RegisterUserRules = new Vector();

Here's a sample SQL statement with tokens:

public static final String TRANSACTION_STRING =
    "Insert into Transactions values ("                                 + TICK + BEGIN + BusinessRules.CONFIRMATION_NUMBER + END
    + TICK + COMMA + SPACE
    + BusinessRules.SYSDATE
    + COMMA + SPACE
    + TICK + BEGIN + BusinessRules.SESSION_ID + END
    + TICK + COMMA + SPACE
    + TICK + BEGIN + BusinessRules.ACTION_NAME + END
    + TICK + COMMA + SPACE
    + TICK + BEGIN + BusinessRules.SEQUENCE_NUMBER + END
    + TICK
    + ")";

The SQL statement is added to the vector, as shown above. It can then be retrieved using reflection in a similar manner to the loadBusinessRules method described in the development of the BusinessObject. I've chosen a simple means by which you can insert values into the SQL string for the greatest flexibility. This scheme does not require that you insert all fields in a particular order; thus, you are freed from a strict interface between business and data objects.

Having loaded the DataRules reflectively using the action name, you can perform the substitution followed by the execution of the SQL against the database. The code for the DefaultDataObject is fairly self-explanatory, with the following methods performing the bulk of the duties:

  • ReplaceTokens: Exchanges the tokenized values with the actual values passed in by the business object
  • BuildSQLString: Parses through the SQL statement and calls the ReplaceTokens method repeatedly
  • Execute: Executes a SQL statement and converts the result set into a vector of vectors

Using this reflective framework, SQL statements can be added, modified, or deleted from the property file from which all of these values should be loaded. This scheme allows the database code to be developed independently of the business objects -- further reducing the development interdependencies.

SUBHEAD_BREAK: Presentation

There are numerous ways to handle the presentation layer -- you can use anything from XML to a tool like HTMLKona. The development timeframe and skill of the development team must be considered before making this decision. In many cases, it is easier to find HTML designers than software developers. A good solution to this situation involves the use of HTML templates, which can include JavaServer Pages.

1 2 3 Page 2
Page 2 of 3