Verify input with Model Objects

Validation of user input is important for almost all applications. There seems to be two places where you may implement such validation logic

Implementing validation in the Model Object has some strong advantages :

Example

Here is a Model Object class named Movie. It performs all its validation in its constructors. If a problem occurs, a checked exception named InvalidUserInput is thrown.


package hirondelle.movies.edit;

import java.math.BigDecimal;
import java.util.*;

import hirondelle.movies.exception.InvalidInputException;
import hirondelle.movies.util.Util;

/**
 Data-centric class encapsulating all fields related to movies. 
 
 <P>This class exists  in order to encapsulate, validate, and sort movie information.
  This class is used both to validate user input, and act as a 'transfer object' when 
  interacting with the database.
 
  <P><b>This class would greatly benefit from a JUnit test class, to test its data 
  validation and sorting.</b>
*/
final class Movie implements Comparable<Movie>{

  /**
   Constructor taking regular Java objects natural to the domain.
   
   <P>When the user has entered text, this constructor is called indirectly, through 
   {@link #Movie(String, String, String, String, String)}.
   
   @param aId optional, the database identifer for the movie. This item is optional since, 
   for 'add' operations,  it has yet to be assigned by the database.
   @param aTitle has content, name of the movie
   @param aDateViewed optional, date the movie was screened by the user
   @param aRating optional, in range 0.0 to 10.0
   @param aComment optional, any comment on the movie
  */
  Movie(
    String aId, String aTitle, Date aDateViewed, BigDecimal aRating, String aComment
  ) throws InvalidInputException {
    fId = aId;
    fTitle = aTitle;
    fDateViewed = aDateViewed;
    fRating = aRating;
    fComment = aComment;
    validateState();
  }
  
  /**
   Constructor which takes all parameters as <em>text</em>.
   
   <P>Raw user input is usually in the form of <em>text</em>.
   This constructor <em>first</em> parses such text into the required 'base objects' - 
   {@link Date}, {@link BigDecimal} and so on. If those parse operations <em>fail</em>, 
   then the user is shown an error message. If N such errors are present in user input, 
   then  N <em>separate</em> message will be presented for each failure, one by one.
   
   <P>If all such parse operations <em>succeed</em>, then the "regular" constructor 
   {@link #Movie(String, String, Date, BigDecimal, String)}
   will then be called. It's important to note that this call to the second constructor 
   can in turn result in <em>another</em> error message being shown to the 
   user (just one this time). 
 */
  Movie(
    String aId, String aTitle, String aDateViewed, String aRating, String aComment
  ) throws InvalidInputException {
      this(
        aId, aTitle, Util.parseDate(aDateViewed, "Date Viewed"), 
        Util.parseBigDecimal(aRating, "Rating"), aComment
      );
  }
  
  String getId(){ return fId; }
  
  /**
   This set method is rather artificial. It results from the toy persistence layer. 
   It's dissatisfying to add this method since the class would otherwise be immutable. 
   Immutability is a highly desirable characteristic.
  */
  void setId(String aId){  fId = aId; }
  
  String getTitle(){ return fTitle; }
  Date getDateViewed(){ return fDateViewed; }
  BigDecimal getRating(){ return fRating; }
  String getComment(){ return fComment; }
  
  @Override public boolean equals(Object aThat){
    if ( this == aThat ) return true;
    if ( !(aThat instanceof Movie) ) return false;
    Movie that = (Movie)aThat;
    return 
      areEqual(this.fTitle, that.fTitle) && 
      areEqual(this.fDateViewed, that.fDateViewed) && 
      areEqual(this.fRating, that.fRating) && 
      areEqual(this.fComment, that.fComment)
    ; 
  }
  
  @Override public int hashCode(){
    int result = 17;
    result = addHash(result, fTitle);
    result = addHash(result, fDateViewed);
    result = addHash(result, fRating);
    result = addHash(result, fComment);
    return result;
  }
  
  @Override public String toString(){
    return 
      "Movie  Id:" + fId + " Title:" + fTitle + " Date Viewed:" + fDateViewed + 
      " Rating:" + fRating + " Comment: " + fComment
    ; 
  }
  
  /** 
   Default sort by Date Viewed, then Title. 
   Dates have the most recent items listed first. 
 */
  public int compareTo(Movie aThat) {
    if ( this == aThat ) return EQUAL;
   
    int comparison = DESCENDING*comparePossiblyNull(this.fDateViewed, aThat.fDateViewed);
    if ( comparison != EQUAL ) return comparison;
    
    comparison = this.fTitle.compareTo(aThat.fTitle);
    if ( comparison != EQUAL ) return comparison;
    
    comparison = comparePossiblyNull(this.fRating, aThat.fRating);
    if ( comparison != EQUAL ) return comparison;
   
    comparison = comparePossiblyNull(this.fComment, aThat.fComment);
    if ( comparison != EQUAL ) return comparison;
    
    return EQUAL;
  }
  
  /** Sort by Title. */
  static Comparator<Movie> TITLE_SORT = new Comparator<Movie>(){
    public int compare(Movie aThis, Movie aThat) {
      if ( aThis == aThat ) return EQUAL;

      int comparison = aThis.fTitle.compareTo(aThat.fTitle);
      if ( comparison != EQUAL ) return comparison;
      
      comparison = DESCENDING*comparePossiblyNull(aThis.fDateViewed, aThat.fDateViewed);
      if ( comparison != EQUAL ) return comparison;
      
      comparison = comparePossiblyNull(aThis.fRating, aThat.fRating);
      if ( comparison != EQUAL ) return comparison;
     
      comparison = comparePossiblyNull(aThis.fComment, aThat.fComment);
      if ( comparison != EQUAL ) return comparison;
      
      return EQUAL;
    };
  };
  
  /** Sort by Rating (descending), then Date Viewed (descending). */
  static Comparator<Movie> RATING_SORT = new Comparator<Movie>(){
    public int compare(Movie aThis, Movie aThat) {
      if ( aThis == aThat ) return EQUAL;

      int comparison = DESCENDING*comparePossiblyNull(aThis.fRating, aThat.fRating);
      if ( comparison != EQUAL ) return comparison;

      comparison = DESCENDING*comparePossiblyNull(aThis.fDateViewed, aThat.fDateViewed);
      if ( comparison != EQUAL ) return comparison;
      
      comparison = aThis.fTitle.compareTo(aThat.fTitle);
      if ( comparison != EQUAL ) return comparison;
      
      comparison = comparePossiblyNull(aThis.fComment, aThat.fComment);
      if ( comparison != EQUAL ) return comparison;
      
      return EQUAL;
    };
  };
  
  /** Sort by Comment. */
  static Comparator<Movie> COMMENT_SORT = new Comparator<Movie>(){
    public int compare(Movie aThis, Movie aThat) {
      if ( aThis == aThat ) return EQUAL;

      int comparison = comparePossiblyNull(aThis.fComment, aThat.fComment);
      if ( comparison != EQUAL ) return comparison;
      
      comparison = aThis.fTitle.compareTo(aThat.fTitle);
      if ( comparison != EQUAL ) return comparison;
      
      comparison = comparePossiblyNull(aThis.fRating, aThat.fRating);
      if ( comparison != EQUAL ) return comparison;

      comparison = DESCENDING*comparePossiblyNull(aThis.fDateViewed, aThat.fDateViewed);
      if ( comparison != EQUAL ) return comparison;
      
      return EQUAL;
    };
  };
  
  // PRIVATE //
  private String fId;
  private final String fTitle;
  private final Date fDateViewed;
  private final BigDecimal fRating;
  private final String fComment;
  private static final BigDecimal TEN = new BigDecimal("10.0");
  private static final int EQUAL = 0;
  private static final int DESCENDING = -1;
  
  private void validateState() throws InvalidInputException {
    InvalidInputException ex = new InvalidInputException();
    
    if( ! Util.textHasContent(fTitle) ) {
      ex.add("Title must have content");
    }
    if ( fRating != null ){
      if ( fRating.compareTo(BigDecimal.ZERO) < 0 ) {
        ex.add("Rating cannot be less than 0.");
      }
      if ( fRating.compareTo(TEN) > 0 ) {
        ex.add("Rating cannot be greater than 10.");
      }
    }
    if ( ex.hasErrors() ) {
      throw ex;
    }
  }
  
  private boolean areEqual(Object aThis, Object aThat){
    return aThis == null ? aThat == null : aThis.equals(aThat);
  }
  
  private int addHash(int aHash, Object aField){
    int result = 37*aHash;
    if (aField != null){
      result = result + aField.hashCode();
    }
    return result;
  }
  
  /** Utility method.  */
  private static <T extends Comparable<T>> int comparePossiblyNull(T aThis, T aThat){
    int result = EQUAL;
    int BEFORE = -1;
    int AFTER = 1;
    
    if(aThis != null && aThat != null){ 
      result = aThis.compareTo(aThat);
    }
    else {
      //at least one reference is null - special handling
      if(aThis == null && aThat == null) {
        //do nothing - they are not distinct 
      }
      else if(aThis == null && aThat != null) {
        result = BEFORE;
      }
      else if( aThis != null && aThat == null) {
        result = AFTER;
      }
    }
    return result;
  }
} 

The user edits Movie data using a dialog (not listed here). When the user hits a button, execution passes to the following MovieController class. The actionPerformed method first attempts to build a Movie object from user input. If a problem is detected, then an error message is displayed to the user.

package hirondelle.movies.edit;

import hirondelle.movies.exception.InvalidInputException;
import hirondelle.movies.main.MainWindow;
import hirondelle.movies.util.Edit;
import hirondelle.movies.util.Util;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Logger;

import javax.swing.JOptionPane;

/**
 Add a new {@link Movie} to the database, or change an existing one. 
 
 <P>It's important to note that this class uses most of the other classes in 
 this feature to get its job done (it doesn't use the <tt>Action</tt> classes):
 <ul>
   <li>it gets user input from the view - {@link MovieView}
   <li>it validates user input using the model - {@link Movie}
   <li>it persists the data using the Data Access Object  - {@link MovieDAO}
 </ul>
*/
final class MovieController implements ActionListener {
  
  /**
   Constructor.
   @param aView user interface
   @param aEdit identifies what type of edit - add or change 
  */
  MovieController(MovieView aView, Edit aEdit){
    fView = aView;
    fEdit = aEdit;
  }
  
  /**
   Attempt to add a new {@link Movie}, or edit an existing one.
   
   <P>If the input is invalid, then inform the user of the problem(s).
   If the input is valid, then add or change the <tt>Movie</tt>, close the dialog, 
   and update the main window's display.  
  */
  public void actionPerformed(ActionEvent aEvent){
    fLogger.fine("Editing movie" + fView.getTitle());
    try {
      createValidMovieFromUserInput();
    }
    catch(InvalidInputException ex){
      informUserOfProblems(ex);
    }
    if ( isUserInputValid() ){
      if( Edit.ADD == fEdit ) {
        fLogger.fine("Add operation.");
        fDAO.add(fMovie);
      }
      else if (Edit.CHANGE == fEdit) {
        fLogger.fine("Change operation.");
        fDAO.change(fMovie);
      }
      else {
        throw new AssertionError();
      }
      fView.closeDialog();
      MainWindow.getInstance().refreshView();
    }
  }

  // PRIVATE //
  private final MovieView fView;
  private Movie fMovie;
  private Edit fEdit;
  private MovieDAO fDAO = new MovieDAO();
  private static final Logger fLogger = Util.getLogger(MovieController.class);
  
  private void createValidMovieFromUserInput() throws InvalidInputException {
    fMovie = new Movie(
      fView.getId(), fView.getTitle(), fView.getDateViewed(), 
      fView.getRating(), fView.getComment()
    );
  }

  private boolean isUserInputValid(){
    return fMovie != null;
  }
  
  private void informUserOfProblems(InvalidInputException aException) {
    Object[] messages = aException.getErrorMessages().toArray();
    JOptionPane.showMessageDialog(
      fView.getDialog(), messages, 
      "Movie cannot be added", JOptionPane.ERROR_MESSAGE
    );
  }
} 



See Also :
Immutable objects
Use a testing framework (JUnit)
Verify input with regular expressions
Input dialogs
Validation belongs in a Model Object
Would you use this technique?
Yes   No   Undecided   
© 2013 Hirondelle Systems | Source Code | Contact | License | RSS
Individual code snippets can be used under this BSD license - Last updated on August 30, 2012.
Over 2,400,000 unique IPs last year - Built with WEB4J.
- In Memoriam : Bill Dirani -