Constraint



Constraint Overview

You can define Constraints that throw exceptions with explanatory text (e.g., for End User correction) if values are not valid.  Constraints can reference:
  • All Domain attribute values
  • All Domain old attribute values
  • Parent Domain attribute values
  • Java/Groovy methods
You specify constraints by defining a method with the @Constraint annotation in a Business Logic Component. This can take two forms.  For simple constraints, you can specify the logic in the annotation with a null method, for example:

import com.autobizlogic.abl.businesslogic.annotations.*;

public class CustomerLogic {

@Constraint(
value = "balance <= creditLimit",
errorMessage = 'Customer {name} balance > credit limit')
public void constraintCreditLimit() { } 

Alternatively, you can provide a method as shown below (e.g., to introduce conditional processing):

import buslogicdemograils.Customer
import com.autobizlogic.abl.businesslogic.annotations.*;
import com.autobizlogic.abl.businesslogicengine.ConstraintFailure;

public class CustomerLogic {
@CurrentBean
public Customer customer;
@Constraint
public void constraintCreditLimit() {
if (customer.isPreferred) {
if (customer.balance > 1.1 * customer.creditLimit)
ConstraintFailure.failConstraint("Preferred credit limit exceeded")
} else {
if (customer.balance > customer.creditLimit)
ConstraintFailure.failConstraint("Customer {name} credit limit exceeded")
}
}


The screen shot below illustrates a common pattern: constraining derived results...


Constraints validate multiple attributes of Domain Object / Parents

Constraints are methods that usually contain a simple expression, often surrounded by null checking. The expression is used to validate an altered Object instance, and follow the rules for formula expressions:

  • Attributes in that Domain Object
    In the screen shot above, the constraintCreditLimit verifies that the balance does not exceed the credit limit
    Note: despite the fact that this is a Groovy class, we have shown a more Java-like syntax. Exploiting Groovy more fully results in a far simpler expression, such as customer.balance > customer.creditLimit
  • Old values in that Domain Object
  • Parent Attributes

Both examples above illustrate the most common Business Logic pattern: Constraining chained derivations:

  • constraintCreditLimit constrains customer.balance (a Sum, also shown in the screen shot)
  • constraintEmptyPurchaseorder constrains purchaseorder.itemCount (a Sum, also shown in the screen shot)


Constraints executed for all altered Objects

For efficiency, the Business Logic Engine only runs rules (including constraints) on Object instances that are altered. 


Includes client updates and Forward Chained updates

Such alterations may be directly by clients, or Forward Chained from logic execution.

For example, the Make Order Ready sample transaction consists of a direct client update to a Purchaseorder, where Sum processing adjusts the related Customer; this would this execute the checkCreditLimit constraint.

By contrast, a simple change to a Purchaseorder.dueDate would only execute Purchaseorder constraints; since this does not incur any Forward ChainingCustomer logic is pruned.


Signaling Constraint Issues

Like other logic rules, constraints are identified by an annotated method.  There are two forms this can take.
The simplest way to define a constraint is to use the annotation parameters to designate the condition and message:

@Constraint(value = "creditRating != 'C' or balance <= 1000",
     errorMessage = "Customer {name}'s balance is too high (${balance}) for credit rating C")
public void constraintCreditLimit() { }


You can also specify the constraint in the method body.  This provides for more complex if/else logic, the ability to invoke Java/Groovy methods, and code completion in the editor.  In most cases, your constraint method will be a simple boolean expression, plus invocation of ConstraintFailure.failConstraint to signal a constraint issue:

import com.autobizlogic.abl.businesslogicengine.constraintexcp.*;

public class CustomerLogic {
    @CommitConstraint
    public void constraintSillyName() {
        if ("Bozo".equals(purchaseorder.approvingOfficer)) 
            ConstraintFailure.failConstraint("$purchaseorder.approvingOfficer is silly")
      }

You can also signal the error attributes associated with the Constraint.  This may allow a GUI to highlight the relevant attributes, for instance. 
The following APIs are provided in the class ConstraintFailure:

public static void failConstraint(String msg)
public static void failConstraint(String msg, String problemAttribute)
public static void failConstraint(String msg, String[] problemAttributes)


Handling Constraints


Rollback is responsibility of commit initiator

During a transaction, if a constraint fails (raises an exception), the transaction will be "poisoned", meaning that it will be marked as un-committable (this is done using a Synchronization object).

The rest of the constraints will be invoked, and additional constraint failures may occur. They will be bundled into one exception, which will be thrown once the logic processing is done.

This exception will be an instance of ConstraintException, which provides API's to retrieve details about the constraint failures.

This ConstraintException should normally be caught by the code that initiated the transaction commit. Because the transaction is "poisoned", it will not be possible to commit that transaction: it must be rolled back.

Note that some frameworks do this automatically: they initiate transactions as required, and rollback automatically if an exception occurs.

Exceptions for Multiple Constraints

If constraints signal bad data:

  1. The Business Logic Engine saves the error, and logic execution continues until all the constraints have been executed for that specific row

  2. When row logic is completed, the system checks if any error(s) have been detected; if so, these are concatenated into a single exception that indicates all the failed constraints.  You will typically display this in a "retry" dialog for the End User to correct their data and re-try the save.

You can explore constraint handling in the BusLogicIntro jUnit test application called Purchaseorder_save_noEmptyOrder_test.  It intentionally causes 2 errors: an empty Purchaseorder (no Lineitems), and an invalid approving officer ("Bozo").  The test assures that both constraints were raised with the following code:

try {

    tx.commit();

} catch (Exception e) {  // avoid in most cases (UI frameworks usually show exceptions)

    tx.rollback()

    constraintsFired = "FIRED, but Message bad"

    System.out.println("\n**** Constraints successfully fired: ****");

    String constraintMsg = "Temporary test error - message unavailable" 

    Throwable cause = e   /* .cause */ 

    if (cause != null)

        constraintMsg = cause.message

    System.out.println("Exception msg: " + constraintMsg + "\n\n");  

    if (constraintMsg.indexOf("at least 1 Line Item") >= 0 &&
        constraintMsg.indexOf(
"silly name"))

        constraintsFired = "ok"

    e.printStackTrace()  // just for debugging, normally not done

}

assert (constraintsFired == "ok")


Exceptions typically not caught

As noted in the code, your Presentation Code (or framework) is not usually required to catch exceptions.  We are doing so to validate that the expected errors are indeed reported.




Constraint Types

The illustration below depicts 2 kinds of constraints, described below.


Normal Constraints

Such constraints are executed as a part of the logic processing as it occurs for client / Forward Chained updates. Unlike Commit Constraints, such logic does not "see" updates in this transaction that are not yet processed.


Commit Constraints

Commit Constraints are executed after all logic processing (client-initiated and Forward Chained) is complete. As such, the effects of all rows have been processed.

The screen shot above illustrates a classic case of Commit Constraints: constraintEmptyPurchaseorder. As you can see, this logic is testing the itemCount (derived in the immediately preceding rule).

To understand this, consider a transaction that process a Purchaseorder and two related Lineitems. Client updates are processed as submitted, so the Purchaseorder is processed first. At this point, the Lineitems have not been processed, so the itemCount is 0.

So, if we were to use a normal constraint, we would always reject the transaction!
By using a Commit Constraint, the constraint logic is deferred until the end of the transaction, in particular when the Lineitems have been processed, and have adjusted the itemCount. So, our sample transaction will succeed. Conversely, a Purchaseorder with no Lineitems will be properly rejected.

Constraint Exceptions

As noted above, constraints raise exceptions if they detect invalid condition on any altered object.  The Business Logic Engine introduces the exception object called ConstraintException, which extends HibernateException.  This object provides accessors for:
  • getMessage(): the text of the constraint

  • getConstraintFailures(): recalling that multiple constraints are raised in a single exception, this enables you to find the list of individual failures (each object is a com.autobizlogic.abl.businesslogicengine.constraintexcp.ConstraintFailure)


Constraint Exception Translation

You can optionally translate the constraints as described below.  For example:
  • Language Translation: you can consolidate all of your translation services to translate constraints into a target language

  • Framework Compliance: some frameworks check the actual class of constraints and take appropriate actions.  They may, for example, test whether exceptions are an instance of javax.validation.ConstraintViolationException

We therefore enable you to supply a ConstraintExceptionTranslator, by following the steps below.


Enable in ABLConfig.properties

Translators are optional, so you must alert the Business Logic Engine you wish to use one.  Add lines such as the following to your ABLConfig.properties file:

##############################################################

# Use this to translate constaints to desired language or java object type (e.g., JCAvs Hibernate)

constraintExceptionTranslator =org.openxava.buslogicdemo.businesslogic.constraintexcp.ConstraintExceptionTranslatorJCA



Implement the Translator class

You provide an implementation of the translator interface as shown below:

public class ConstraintExceptionTranslatorJCA implements ConstraintExceptionTranslator {
  public RuntimeException translateConstraintException(

                             ConstraintException aConstraintExcp) {

Note: if you are translating to javax objects, you will need to translate the List of aConstraintExcp's com.autobizlogic.abl.businesslogicengine.constraintexcp.ConstraintFailure to the javax equivalent javax.validation.ConstraintViolation



Comments