Reusable Data Constraints

When placing data into the data model, you allow users to place practically anything into the various fields as long as they use the correct data type. For example, the user could input null for the interest rate and crash the system when it tries to calculate the interest. Even worse, a user could easily input -5.45 for the interest rate on her credit card. The bank may find it to be a little expensive to pay the customer 545% interest for running up her credit card! Obviously, you need to put some constraints on the data members throughout the model. For the problematic interest rate, the natural thing to do would be to add checking code to the setter of the interest rate in the LiabilityAccount class. This code would prevent any negative values and constrain the rate to 1.0 or less. Your modified property setter would look like the following:

public void setInterestRate(final Float interestRate) {
  if ((interestRate.floatValue( ) > 1.0f) || 
 (interestRate.floatValue( ) < 0.0f)) {
 throw new IllegalArgumentException( );
 }
 final Float oldInterestRate = this.interestRate;
 this.interestRate = interestRate;
 propertyChangeSupport.firePropertyChange("interestRate", oldInterestRate, this.interestRate);
}


Although this checking code will work, it isn't very elegant or reusable. For example, if the GUI for your bank wants to display an entry field for the interest rate, it would have no idea what the legal values are. Also, if you hardcode the legal values into the GUI, you would have two sets of validation code to maintain. Instead, you should write a validator that can validate the interest rate whenever it is needed. Even better, the validator could tell you which rules it is applying so you could display them in a tool tip or in a text field in a web form. You can do this by designing special-purpose validator objects that can be interrogated by other pieces of code to find out their rules. To start with, you will need a way to indicate that a validation failed.

The ConstraintException

To facilitate the validation, you need to create a runtime exception that you can throw if validation fails. You can make ConstraintException a RuntimeException subclass because the concept of ConstraintException is similar to IllegalArgumentException.

The Constraint Hierarchy

Now that you can indicate if a validation failed, you need to be able to perform the data validation. Your validator should implement essentially the same code used in the embedded validation code. It should also provide information on the rules it enforces. To do this, introduce a hierarchy of classes known as constraints, which are reusable and immutable objects that validate pieces of data. The Constraint class shown in Example 8-2 forms the base of your new hierarchy.

Example 8-2. The base of all constraints
package oracle.hcj.datamodel.constraints;
public abstract class Constraint {
 private String name;
 protected Constraint(final String name) {
 this.name = name;
 }
 public String getName( ) {
 return this.name;
 }
}


This is a basic abstract constraint class. Since all constraints in the architecture should name the property they are constraining, it is useful to have this in the base class. It isn't until you get to the derived classes that the code actually starts to get interesting.

Primitive constraints

After the base class, you have to split the hierarchy into two directions: one that is suitable for validating primitives, and one that is suitable for validating objects. The code for the primitives is rather mundane and repetitive. Since primitives are not objects, you need one validator for each type of primitive; they all follow the same pattern as IntValidator, shown in Example 8-3.

Example 8-3. The int type constraint
package mirror.datamodel.constraints;
public class IntConstraint extends Constraint {
 private int maxValue;
 private int minValue;
 public IntConstraint(final String name, final int minValue, final int maxValue) {
 super(name);
 this.minValue = minValue;
 this.maxValue = maxValue;
 }
 public int getMaxValue( ) {
 return this.maxValue;
 }
 public int getMinValue( ) {
 return this.minValue;
 }
 public void validate(final int value) {
 if (minValue > value) {
 throw new ConstraintException(
 ConstraintExceptionType.VALUE_BELOW_MINIMUM);
 }
 if (maxValue < value) {
 throw new ConstraintException(
 ConstraintExceptionType.VALUE_ABOVE_MAXIMUM);
 }
 }
}


Most of this code is mundane, but it demonstrates the basic plan for all constraints. Ideally, the user would build an immutable validator with the legal values of the property. Whenever the user tries to set that property, the setter would call validate( ) with the integer to be validated as a parameter. If the rules are broken, you would get an exception. Also, another piece of code can ask this validator what its minimum and maximum values are through its getMinValue( ) and getMaxValue( ) methods. For example, a GUI could use these methods to configure a spinner control with minimal effort and coding. You will need one of these primitive validators for float, one for long, and so on. Unfortunately, other than the change in the data types, the validators for primitives are all identical. However, they have all been prepared for you. You can find them in the mirror.datamodel.constraints package.

Object constraints

Constraints get more interesting when you consider how they apply to objects. Objects themselves can be null-valued, which is one of the core reasons why wrapper types are used throughout the model. Example 8-4 illustrates the basic object constraint.

Example 8-4. The object constraint
package mirror.datamodel.constraints;
public class ObjectConstraint extends Constraint {
 private static final String ERR_PRIMITIVE = "The dataType cannot be a primitive";
 /** Holds value of property dataType. */
 private Class dataType;
 /** Holds value of property optional. */
 private boolean optional;
 public ObjectConstraint(final String name, final boolean optional, final Class dataType) {
 super(name);
 if (dataType.isPrimitive( )) {
 throw new IllegalArgumentException(ERR_PRIMITIVE);
 }
 this.optional = optional;
 this.dataType = dataType;
 }
 public Class getDataType( ) {
 return this.dataType;
 }
 public boolean isOptional( ) {
 return this.optional;
 }
 public void validate(final Object obj) {
 if (obj == null) {
 if (!optional) {
 throw new ConstraintException(
 ConstraintExceptionType.NULL_NOT_ALLOWED);
 }
 } else if (!dataType.isAssignableFrom(obj.getClass( ))) {
 throw new ConstraintException(
 ConstraintExceptionType.INVALID_DATA_TYPE);
 }
 }
}


This class allows you to validate any kind of object to determine whether it should allow the value to be null. Since primitives cannot be null, this class does not allow you to create a constraint with int.class or with any other primitive class. To accomplish this, it simply asks the class that you pass in whether it is a primitive class.

Screenshot

Using isPrimitive( ) in the validator is a basic component of reflection. We will cover this and the other reflection methods in .


In the ObjectConstraint validate( ) method, the data type is validated and it is determined whether a null value is allowed. In this manner, you can easily use the method to verify simple properties. A snippet of the Account class from your data model shows how it is used:

package oracle.hcj.bankdata;
public abstract class Account extends MutableObject {
 /** Constraint for the customer property. */
 public static final ObjectConstraint CUSTOMER_CONSTRAINT =
 new ObjectConstraint("customer", false, Customer.class);
 /** * Setter for property customer.
 *
 * @param customer New value of property customer.
 */
 public void setCustomer(final Customer customer) {
 CUSTOMER_CONSTRAINT.validate(customer);
 final Customer oldCustomer = this.customer;
 this.customer = customer;
 propertyChangeSupport.firePropertyChange("customer", oldCustomer, this.customer);
 }
}


Once the user creates the constraint, it can be easily used in the setter. The proposed change to the setter is passed to the validator, and the data is validated according to optional requirements and data type. As a bonus, a dynamic web engine can interrogate CUSTOMER_CONSTRAINT to determine whether it should place a red star next to the field in the form to indicate that the field is required.

A numerical object constraint

One of the benefits of object validators is that they allow you to validate all numerical primitive wrappers in one validator. See Example 8-5.

Example 8-5. A numerical validator
package mirror.datamodel.constraints;
public class NumericConstraint extends ObjectConstraint {
 private static final String ERR_NOT_NUMBER = "The dataType must be a Number";
 private static final String ERR_MISMATCH =
 "The min and max value type must match the data type.";
 private Number maxValue;
 private Number minValue;
 public NumericConstraint(final String name, final boolean optional,
 final Class dataType, final Number minValue,
 final Number maxValue) {
 super(name, optional, dataType);
 if (!Number.class.isAssignableFrom(dataType)) {
 throw new IllegalArgumentException(ERR_NOT_NUMBER);
 }
 if (!(minValue.getClass( ).equals(dataType))) {
 throw new IllegalArgumentException(ERR_MISMATCH);
 }
 if (!(maxValue.getClass( ).equals(dataType))) {
 throw new IllegalArgumentException(ERR_MISMATCH);
 }
 // ** validation done
 this.minValue = minValue;
 this.maxValue = maxValue;
 }
 public Number getMaxValue( ) {
 return this.maxValue;
 }
 public Number getMinValue( ) {
 return this.minValue;
 }
 public void validate(final Object obj) {
 super.validate(obj);
 if (obj != null) {
 if (((Comparable)obj).compareTo(minValue) < 0) {
 throw new ConstraintException(
 ConstraintExceptionType.VALUE_BELOW_MINIMUM);
 }
 if (((Comparable)obj).compareTo(maxValue) > 0) {
 throw new ConstraintException(
 ConstraintExceptionType.VALUE_ABOVE_MAXIMUM);
 }
 }
 }
}


NumericConstraint takes advantage of the fact that all of the wrapper types extend the Number class and implement Comparable. Instead of defining one constraint for each type of number, you can do them all in one swipe. This numerical validator will work for every numerical wrapper type. It first invokes the superclass ObjectConstraint to validate data type and optionality and then proceeds to validate the range. Setting up the NumericConstraint is similar to using the object validator; the validator simply requires a few more arguments:

package oracle.hcj.bankdata;
public abstract class Account extends MutableObject {
 /** Constraint for the ID property. */
 public static final NumericConstraint ID_CONSTRAINT =
 new NumericConstraint("ID", false, Integer.class, new Integer(1000000),
 new Integer(Integer.MAX_VALUE));
}


Once the constraint is set up, use it as you would an ObjectConstraint.

Constraints for collections

Validating collections is a bit more complicated than validating primitives or numerical types because of the lack of parameterized types in the JDK. Parameterized types should be familiar to those of you who have experience in C++—they allow you to make type-safe collections and maps. Since no bad data can get into a type-safe set, this makes the validation much simpler. Unfortunately, since a user can place any kind of object in your set, which is supposed to contain only Account objects, you will have to check not only the type of the set but the entire contents of the set as well. Many programmers have tried to compensate for the lack of type safety in Java collections, but all of their solutions have serious drawbacks. For example, source code generation can significantly increase the size of the source code base, which is a distinct project risk; simply put, the more code you have, the higher the potential for bugs. Unfortunately, there isn't a quick solution, so you have to do your validation the hard way. To illustrate how to validate a collection, here's the validate( ) method of the CollectionConstraint class:

package mirror.datamodel.constraints;
public class CollectionConstraint extends ObjectConstraint {
 public void validate(final Object obj) {
 if (obj == null) {
 throw new ConstraintException(
 ConstraintExceptionType.NULL_NOT_ALLOWED);
 }
 if (!getDataType( ).isAssignableFrom(obj.getClass( ))) {
 throw new ConstraintException(
 ConstraintExceptionType.INVALID_DATA_TYPE);
 }
 Collection coll = (Collection)obj;
 if (coll.isEmpty( ) && (!isOptional( )) {
 throw new ConstraintException(
 ConstraintExceptionType.COLLECTION_CANNOT_BE_EMPTY);
 }
  if (containedType != null) {
 Iterator iter = coll.iterator( );
 while (iter.hasNext( )) {
 if (!(iter.next( ).getClass( ).equals(containedType))) {
 throw new ConstraintException(
 ConstraintExceptionType.INVALID_SET_MEMBER);
 }
 }
 }
 }
}


If the emphasized code seems to be a bit bulky and a performance hog, you are correct. However, you have little choice—if you don't validate the collection data type itself, along with every object inside the collection, a user could place an invalid object in the collection. But the setters won't be called that often. In the interest of a solid data model, you will have to deal with the performance overhead of calling setters on collection methods. By contrast, the same validation with parameterized types would look like the following:

public void validate(final Object obj) {
 if (obj == null) {
 throw new ConstraintException(ConstraintExceptionType.
 NULL_NOT_ALLOWED);
 }
 if (!getDataType( ).isAssignableFrom(obj.getClass( ))) {
 throw new ConstraintException(ConstraintExceptionType.
 INVALID_DATA_TYPE);
 }
 Collection coll = (Collection)obj;
 if (coll.isEmpty( ) && (!isOptional( )) {
 throw new ConstraintException(
 ConstraintExceptionType.COLLECTION_CANNOT_BE_EMPTY);
 }
}


In this version, you don't have to validate the objects in the collection because the user would describe his collections, as in the following code:

/** The list of transactions executed on thsi account. */
private Set transactionList = new HashSet<Transaction>( );


This example declares a HashSet that can have members of type Transaction only. Since the set declared in this example is type-safe, you can be certain there are no illegal values inside it. However, until parameterized types are available, you will need to make a series of validators the hard way. Note that you will need only a Collection validator and a Map validator. All of the collection and map classes implement these two interfaces, and you do not need any special features of their implementation classes for the validator. Therefore, you can get away with two validators only.

Using Constraints

Throughout the data model, you use constraints to bind data only within accepted ranges. This allows you to build interfaces that do not depend on hardcoded values. Interactive constraints will open up your data model so it can provide much more information than if you had coded each property individually. Also, since the constraints are reusable, you can easily constrain data in other apps. The Account class in the oracle.hcj.bankdata package shows how several types of constraints are used. First, the class sets up its constraints as final objects:

package oracle.hcj.bankdata;
public abstract class Account extends MutableObject {
 /** Constraint for the ID property. */
 public static final NumericConstraint ID_CONSTRAINT =
 new NumericConstraint("ID", false, Integer.class, new Integer(1000000),
 new Integer(Integer.MAX_VALUE));
 /** Constraint for the customer property. */
 public static final ObjectConstraint CUSTOMER_CONSTRAINT =
 new ObjectConstraint("customer", false, Customer.class);
 /** Constraint for the balance property. */
 public static final NumericConstraint BALANCE_CONSTRAINT =
 new NumericConstraint("balance", false, Float.class, new Float(Float.MIN_VALUE),
 new Float(Float.MAX_VALUE));
 /** Constraint for the transactionList property. */
 public static final CollectionConstraint TRANSACTION_LIST_CONSTRAINT =
 new CollectionConstraint("transactionList", false, Set.class,
 Transaction.class);
}


Here, several constraints are defined for the various properties of Account. Note that a common naming standard was used to define these constraints; each constraint is in uppercase with the words separated by underscores, followed by the _CONSTRAINT suffix. (This will be useful later when you try to look up constraints dynamically.) Also, defining the constraints as final objects in the class means that if you ever have to change a range for a particular property, you can do it in one place, and all of the users of the constraint will be updated automatically. This will save you enormous amounts of time when your requirements change. Once the constraints are set up, each individual setter uses them:

package oracle.hcj.bankdata;
public abstract class Account extends MutableObject {
 public void setBalance(final Float balance) {
 BALANCE_CONSTRAINT.validate(balance);
 final Float oldBalance = this.balance;
 this.balance = balance;
 propertyChangeSupport.firePropertyChange("balance", oldBalance, this.balance);
 }
}


Since the ConstraintException is a RuntimeException subtype, you don't need to declare it in the throws clause of the method. If the validate( ) method fails, the appropriate ConstraintException will be thrown, which will cause the method to exit without changing any members of the class. Using the constraints in a GUI is also a simple matter. For example, you can use INTEREST_RATE_CONSTRAINT from LiabilityAccount to validate data in a GUI. Example 8-6 shows a dialog box for entering interest rates for liability accounts.

Example 8-6. An interest rate dialog
package oracle.hcj.datamodeling;
public class InterestRateDialog extends JDialog implements ActionListener {
 private JButton okBtn;
 private JTextField interestRateField;
 private InterestRateDialog( ) {
 setModal(true);
 final Container contentPane = getContentPane( );
 contentPane.setLayout(new GridLayout(3,1));
 JLabel interestRateLabel =
 new JLabel("Enter an Interest Rate (Between " +
 LiabilityAccount.INTEREST_RATE_CONSTRAINT
 .getMinValue( ) + " and " +
 LiabilityAccount.INTEREST_RATE_CONSTRAINT
 .getMaxValue( ) + ")");
 contentPane.add(interestRateLabel);
 interestRateField = new JTextField(15);
 contentPane.add(interestRateField);
 okBtn = new JButton("OK");
 okBtn.addActionListener(this);
 contentPane.add(okBtn);
 pack( );
 }
 /** * Run the demo.
 *
 * @param args Command line arguments.
 */
 public static final void main(final String[] args) {
 InterestRateDialog demo = new InterestRateDialog( );
 demo.show( );
 System.exit(0);
 }
 public void actionPerformed(final ActionEvent event) {
 Object src = event.getSource( );
 if (src == okBtn) {
 float value = Float.parseFloat(interestRateField.getText( ));
 try {
 LiabilityAccount.INTEREST_RATE_CONSTRAINT
 .validate(new Float(value));
 // do something with the value. dispose( );
 } catch (final ConstraintException ex) {
 Toolkit.getDefaultToolkit( ).beep( );
 interestRateField.requestFocus( );
 interestRateField.selectAll( );
 }
 }
 }
}


The dialog in the constructor interrogates the INTEREST_RATE_CONSTRAINT to find out what the minimum and maximum values are. It then concatenates these values to the label. In the actionPerformed( ) method, the constraint is used to check the input value to see whether it is a legal value for interest rates. If it isn't, the user is prompted to enter a valid interest rate. The reusable nature of constraints makes it easy to build GUIs that check values. Also, you can use a constraint in a JSP page to annotate a form. There are many possible uses of constraints. Once you learn about reflection in , you will be able to discover these constraints dynamically, which will make them even more useful.

Creating New Constraint Types

In the package mirror.datamodel.constraints, there are a wide variety of constraints that can be used with any data model you care to write. Each is constructed in a similar manner as the NumericConstraint class. You should need to create a new constraint type only when the code for validation is significantly different than the constraints that come with Mirror. Do not use constraints as a substitute for proper checking on construction. For example, you could create a constraint for the java.awt.Color class to validate that the red, green, and blue values are between 0 and 256. However, this is misplaced logic; it is the Color class itself that should do validation upon construction, not an outside constraint. Furthermore, if only a set of distinct colors is allowed, the best bet would be to create a constant object class instead of a custom constraint. Generally, you should have to create new constraint types only if you want to use new data types in your model that have a range of possibilities, but only a few of them are legal in the context of your class. Essentially, constraints check legality based on context and not on type, and should be created only when unique types of contexts present themselves.

      
Comments