Skip Headers
Oracle® Application Development Framework Developer's Guide For Forms/4GL Developers
10g Release 3 (10.1.3.0)

Part Number B25947-02
Go to Documentation Home
Home
Go to Table of Contents
Contents
Go to Index
Index
Go to Feedback page
Contact Us

Go to previous page
Previous
Go to next page
Next
View PDF

9 Implementing Programmatic Business Rules in Entity Objects

This chapter explains the key entity object events and features for implementing the most common kinds of business rules.

This chapter includes the following sections:

9.1 Introduction to Programmatic Business Rules

Complementing its built-in declarative validation features, entity objects have method validators and several events you can handle to implement encapsulated business logic using Java code. By the end of this chapter, you'll understand all the concepts illustrated in Figure 9-1, and more:

Figure 9-1 Key Entity Objects Features and Events for Programmatic Business Logic

Image shows entity object features

9.2 Understanding the Validation Cycle

Each entity row tracks whether or not its data is valid. When an existing entity row is retrieved from the database, the entity is assumed to be valid. When the first persistent attribute of an existing entity row is modified, or when a new entity row is created, the entity is marked invalid.

In addition, since a composed child entity row is considered an integral part of its composing parent entity object, any change to composed child entity rows causes the parent entity to be marked invalid.

When an entity is in an invalid state, the declarative validation you have configured and the programmatic validation rules you have implemented are evaluated again before the entity can be consider valid again. You can determine whether a given entity row is valid at runtime by calling the isValid() method on it.

9.2.1 Types of Entity Object Validation Rules

Entity object validation rules fall into two basic categories: attribute-level and entity-level.

9.2.1.1 Attribute-Level Validation Rules

Attribute-level validations are triggered for a particular entity object attribute when either the end user or program code attempts to modify the attribute's value. Since you cannot determine the order in which attributes will be set, attribute-level validation rules should be only used when the success or failure of the rule depends exclusively on the candidate value of that single attribute.

The following examples are attribute-level validations:

  • The value of the AssignedDate of a service request should not be a date in the past.

  • The ProdId attribute of a service request should represent an existing product.

9.2.1.2 Entity-Level Validation Rules

All other kinds of validation rules are entity-level validation rules. These are rules whose implementation requires considering two or more entity attributes, or possibly composed children entity rows, in order to determine the success or failure of the rule.

The following examples are attribute-level validations.

  • The value of the AssignedDate of a service request should be a data that comes after the RequestDate.

  • The ProdId attribute of a service request should represent an existing product.

Entity-level validation rules are triggered by calling the validate() method on a Row. This will occur when:

  • You call the method explicitly on the entity object

  • You call the method explicitly on a view row with an entity row part that is invalid

  • A view object's iterator calls the method on the current row in the view object before allowing the current row to change

  • Transaction commit processing validates an invalid entity in the pending changes list before proceeding with posting the changes to the database.

9.2.2 Understanding Commit Processing and Validation

Transaction commit processing happens in three basic phases:

  1. Ensure any invalid entity rows on the pending changes list are valid.

  2. Post the pending changes to the database by performing appropriate DML operations.

  3. Commit the transaction.

If you have business validation logic in your entity objects that executes queries or stored procedures that depends on seeing the posted changes in the SELECT statements they execute, they should be coded in the beforeCommit() method described in Section 9.6.3, "Validating Conditions Related to All Entities of a Given Type". This method fires after all DML has been applied so queries or stored procedures invoked from that method can "see" all of the pending changes that have been saved, but not yet committed.

Caution:

The transaction-level postChanges() method that exists to force the transaction to post unvalidated changes without committing them is not recommended for use in web applications unless you can guarantee that the transaction will definitely be committed or rolled-back during the same HTTP request. Failure to heed this advice can lead to strange results in an environment where both application modules and database connections can be pooled and shared serially by multiple different clients.

9.2.3 Avoiding Infinite Validation Cycles

If your validation rules contain code that updates attributes of the current entity or other entities, then the act of validating the entity can cause that or other entities to become invalid. As part of the transaction commit processing phase that attempts to validate all invalid entities in the pending changes list, the transaction will perform up to 10 passes on the pending changes list in an attempt to reach a state where all pending entity rows are valid.

If after 10 passes, there are still invalid entities in the list, you will see the following exception:

JBO-28200: Validation threshold limit reached. Invalid Entities still in cache

This is as sign that you need to debug your validation rule code to avoid inadvertently invalidating entities in a cyclic fashion.

9.2.4 What Happens When Validations Fail

When an entity object's validation rules throw exceptions, the exceptions are bundled and returned to the client. If the validation failures are thrown by methods you've overridden to handle events during the transaction postChanges processing, then the validation failures cause the transaction to rollback any database INSERT, UPDATE, or DELETE statements that might have been performed already during the current postChanges cycle.

9.2.5 Understanding Entity Objects Row States

When an entity row is in memory, it has an entity state that reflects the logical state of the row. Figure 9-2 illustrates the different entity row states and how an entity row can transition from one state to another. When a entity row is first created, its status is New. You can use the setNewRowState() method to mark the entity as being Initialized, which removes it from the transaction's list of pending changes until the user sets at least one of its attributes, at which time it returns to the New state. The Unmodified state reflects an entity that has been retrieved from the database and has not yet been modified. It is also the state that a New or Modified entity transitions too after the transaction successfully commits. During the transaction in which it is pending to be deleted, an Unmodified entity row transitions to the Deleted state. Finally, if a row that was New and got removed before the transaction commits, or Unmodified and got successfully deleted, the row transition to the Dead state.

Figure 9-2 Diagram of Entity Row States and Transitions

Image of diagram of entity row states and transitions

You can use the getEntityState() method to access the current state of an entity row in your business logic code.

Note:

If you use the postChanges() method to post pending changes without committing the transaction yet, the getPostState() method returns the entity's state from the point of view of it's being posted to the database or not. For example, a new entity row that has been inserted into the database due to your calling the postChanges()method programmatically — but which has not yet been committed — will have a different value for getPostState() and getEntityState(). The getPostState() method will reflect an "Unmodified" status since the new row has been posted, however the getEntityState() will still reflect that the entity is New in the current transaction.

9.3 Using Method Validators

Method validators are the primary way Oracle recommends supplementing declarative validation rules using your own Java code. Method validators trigger Java code that you write in your own validation methods at the appropriate time during the entity object validation cycle. You can add any number of attribute-level method validators or entity-level method validators, provided they each trigger a distinct method name in your code. All validation method names must begin with the word validate; however, following that rule you are free to name them in any way that most clearly identifies there functionality.

9.3.1 How to Create an Attribute-Level Method Validation

To create an attribute-level method validator:

  1. Open the Entity Object Editor

  2. If your entity object does not yet have a custom Java class, then first open the Java page and enable the generation of an Entity Object Class, and click Apply to generate the *.java file.

  3. Open the Validation page and select the attribute that you want to validate.

  4. Click New to add a validation rule.

  5. Select the Method Validator type from the Rule dropdown list, as shown in Figure 9-3.

The Add Validation Rule dialog displays the expected method signature for an attribute-level validation method. You have two choices:

  • If you already have a method in your entity object's custom Java class of the appropriate signature, it will appear in the list and you can select it after unchecking the Create and Select Method checkbox.

  • If you leave the Create and Select Method checked, you can enter any method name in the Method Name box that begins with the word validate and when you click OK, JDeveloper will add that method to your entity object's custom Java class with the appropriate signature.

Finally, supply the text of the error message for the default locale that the end user should see if this validation rule fails.

Figure 9-3 Adding an Attribute-Level Method Validator

Image of Add Validation Rule dialog

9.3.2 What Happens When You Create an Attribute-Level Method Validator

When you add a new method validator, JDeveloper updates the XML component definition to reflect the new validation rule. If you asked to have the method created, the method is added to the entity object's custom Java class. Example 9-1 illustrates a simple attribute-level validation rule that ensures the AssignedDate of a service request is a date in the current month. Notice that the method accepts an argument of the same type as the corresponding attribute, and that its conditional logic is based on the value of this incoming parameter. When the attribute validator fires, the attribute value has not yet been set to the new value in question, so calling the getAssignedDate() method inside the attribute validator for the AssignedDate attribute would return the attribute's current value, rather than the candidate value that the client is attempting to set.

Example 9-1 Simple Attribute-Level Method Validator

// In ServiceRequestImpl.java in SRDemo Sample
public boolean validateAssignedDate(Date  data) {
  if (data != null && data.compareTo(getFirstDayOfCurrentMonth()) <= 0) {
    return false;
  }
  return true;
}

Note:

The return value of the compareTo() method is zero (0) if the two dates are equal, negative one (-1) if the first date is less than the second, or positive one (1) if the first date is greater than the second.

9.3.3 How to Create an Entity-Level Method Validator

To create an entity-level method validator:

  1. Open the Entity Object Editor.

  2. If your entity object does not yet have a custom Java class, then first open the Java page and enable the generation of an Entity Object Class, and click Apply to generate the *.java file.

  3. Open the Validation page and select root node in the tree that represents the entity object itself.

  4. Click New to add a validation rule.

  5. Select the Method Validator type from the Rule dropdown list, as shown in Figure 9-4.

The Add Validation Rule dialog displays the expected method signature for an entity-level validation method. You have two choices:

  • If you already have a method in your entity object's custom Java class of the appropriate signature, it will appear in the list and you can select it after unchecking the Create and Select Method checkbox

  • If you leave the Create and Select Method checked, you can enter any method name in the Method Name box that begins with the word validate and when you click OK JDeveloper will add that method to your entity object's custom Java class with the appropriate signature.

Finally, supply the text of the error message for the default locale that the end user should see if this validation rule fails.

Figure 9-4 Adding an Entity-Level Method Validator

Image of Add Validation rule dialog

9.3.4 What Happens When You Create an Entity-Level Method Validator

When you add a new method validator, JDeveloper updates the XML component definition to reflect the new validation rule. If you asked to have the method created, the method is added to the entity object's custom Java class. Example 9-2 illustrates a simple entity-level validation rule that ensures the AssignedDate of a service request comes after the RequestDate.

Example 9-2 Simple Entity-Level Method Validator

public boolean validateAssignedDateAfterRequestDate() {
  Date assignedDate = getAssignedDate();
  Date requestDate  = getRequestDate();
  if (assignedDate != null && assignedDate.compareTo(requestDate) < 0) {
    return false;
  }
  return true;
}

9.3.5 What You Might Need To Know About Translating Validation Rule Error Messages

Like the locale-specific UI control hints for entity object attributes, the validation rule error messages are added to the entity object's component message bundle file. These represent the strings for the default locale for your application. To provide translated versions of the validation error messages, follow the same steps as for translating the UI control hints that you've seen in previous chapters.

9.3.6 What You May Need to Know About Referencing the Invalid Value in an Attribute-Level Validation Error Message

The validation error message you supply when adding an attribute-level validation rule can reference the invalid value by referencing the message parameter token "{3}" in the string. The other error parameters supplied are useful for programmatic processing of the ValidationException, but not typically useful in the message string itself.

9.4 Assigning Programmatically-Derived Attribute Values

When declarative defaulting falls short of your needs, you can perform programmatic defaulting in your entity object:

9.4.1 Defaulting Values for New Rows at Create Time

The create() method provides the entity object event you can handle to initialize default values the first time an entity row is created. Example 9-3 shows the overridden create method of the ServiceHistory entity object in the SRDemo application. It calls the attribute setter methods to populate the SvhType, CreatedBy, and LineNo attributes in a new service history entity row.

Example 9-3 Programmatically Defaulting Attribute Values for New Rows

// In ServiceHistoryImpl.java in SRDemo sample
protected void create(AttributeList nameValuePair) {
  super.create(nameValuePair);
  setSvhType(getDefaultNoteType());
  setCreatedBy(getCurrentUserId());
  setLineNo(new Number(getServiceRequest().getMaxHistoryLineNumber()+1));
}

9.4.1.1 Choosing Between create() and initDefaults() Methods

If an entity row has New status and you call the refresh() method on it, if you do not supply either the REFRESH_REMOVE_NEW_ROWS or REFRESH_FORGET_NEW_ROWS flag, then the entity row is returned to an Initialized status. As part of this process, the entity object's initDefaults() method is invoked, but not its create() method again. So override the initDefaults() method for programmatic defaulting logic that you want to fire both when the row is first created, as well as when it might be refreshed back to initialized status.

9.4.1.2 Eagerly Defaulting an Attribute Value from a Database Sequence

Section 6.6.3.7, "Synchronization with Trigger-Assigned Values" explained how to use the DBSequence type for primary key attributes whose values need to be populated by a database sequence at commit time. Sometimes you may want to eagerly allocate a sequence number at entity row creation time so that the user can see its value and so that this value does not change when the data is saved. To accomplish this, use the SequenceImpl helper class in the oracle.jbo.server package in an overridden create() method as shown in Example 9-4. It shows code from the custom Java class of the SRDemo application's Product entity object. After calling super.create(), it creates a new instance of the SequenceImpl object, passing the sequence name and the current transaction object. Then it calls the setProdId() attribute setter method with the return value from SequenceImpl's getSequenceNumber() method.

Example 9-4 Eagerly Defaulting an Attribute's Value from a Sequence at Create Time

// In ProductImpl.java
import oracle.jbo.server.SequenceImpl;
// Default ProdId value from PRODUCTS_SEQ sequence at entity row create time
protected void create(AttributeList nameValuePair) {
  super.create(nameValuePair);
  SequenceImpl sequence = new SequenceImpl("PRODUCTS_SEQ",getDBTransaction());
  setProdId(sequence.getSequenceNumber());
}

9.4.2 Assigning Derived Values Before Saving

If you want to assign programmatic defaults for entity object attribute values before a row is saved, override the prepareForDML() method and call the appropriate attribute setter methods to populate the derived attribute values. In order to perform the assignment only during INSERT, UPDATE, or DELETE, you can compare the value of the operation parameter passed to this method against the integer constants DML_INSERT, DML_UPDATE, DML_DELETE respectively.

Example 9-5 shows the overridden prepareForDML() method used by the ServiceHistory entity object in the SRDemo application to automatically change the status of a service request when a service history note of certain types are created. When a new service history entry is inserted, this code changes the status of:

  • A pending or closed service request to open if the new history note is added by a customer

  • An open service request to pending if the new history note is added by a technician

Example 9-5 Assigning Derived Values Before Saving Using PrepareForDML

// In ServiceHistoryImpl.java
protected void prepareForDML(int operation, TransactionEvent e) {
  super.prepareForDML(operation, e);
  // If we are inserting a new service history entry... 
  if (operation == DML_INSERT) {
    ServiceRequestImpl serviceReq = getServiceRequest();
    String historyType = getSvhType();
    // If request is pending or closed and customer adds note, status => Open
    if ((serviceReq.isPending() || serviceReq.isClosed())
        && CUSTOMER_TYPE.equals(historyType)) {
        serviceReq.setOpen();
    }
    // If request is open & technician adds a non-hidden note, status => Pending
    if (serviceReq.isOpen() && TECHNICIAN_TYPE.equals(historyType)) {
        serviceReq.setPending();
    }
  }
}

9.4.3 Assigning Derived Values When an Attribute Value is Set

To assign derived attribute values whenever another attribute's value is set, add code to the latter attribute's setter method. Example 9-6 shows the setter method for the AssignedTo attribute in the SRDemo application's ServiceRequest entity object. After the call to setAttributeInternal() to set the value of the AssignedTo attribute, it uses the setter method for the AssignedDate attribute to set its value to the current date and time.

Example 9-6 Setting the Assigned Date Whenever the AssignedTo Attribute Changes

// In ServiceRequestImpl.java
public void setAssignedTo(Number value) {
  setAttributeInternal(ASSIGNEDTO, value);
  setAssignedDate(getCurrentDateWithTime());
}

Note:

It is safe to add custom code to the generated attribute getter and setter methods as shown here. When JDeveloper modifies code in your class, it intelligently leaves your custom code in place.

9.5 Undoing Pending Changes to an Entity Using the Refresh Method

You can use the refresh(int flag) method on any Row to refresh pending changes it might have. The refresh() method's behavior depends on the flag that you pass as a parameter. The three key flag values that control its behavior are the following constants in the Row interface.

9.5.1 Controlling What Happens to New Rows During a Refresh

By default, any entity rows with New status that you refresh() are reverted back to blank rows in the Initialized state. Declarative defaults are reset, as well as programmatic defaults coded in the initDefaults() method, but the entity object's create() method is not invoked during this blanking-out process.

You can change this default behavior by combining one of the following two flags with one from the above section (using the bitwise-OR operator):

  • REFRESH_REMOVE_NEW_ROWS, new rows are removed during refresh.

  • REFRESH_FORGET_NEW_ROWS, new rows are marked Dead.

9.5.2 Cascading Refresh to Composed Children Entity Rows

You can cause a refresh() operation to cascade to composed child entity rows by bitwise-OR'ing the REFRESH_CONTAINEES flag with any of the valid flag combinations above. This causes the entity to invoke refresh() using the same mode on any composed child entities it contains.

9.6 Using View Objects for Validation

When your business logic requires performing SQL queries, the natural choice is to use a view object to perform that task. Keep in mind that SQL statements you execute for validation will "see" pending changes in the entity cache if they are entity-based view objects; read-only view objects will only retrieve data that has been posted to the database.

9.6.1 Creating View Objects at Runtime for Validation

Since entity objects are designed to be reused in any application scenario, they should not depend directly on a view object instance in any specific application module's data model. Doing so would prevent them from being reused in other application modules, which is highly undesirable.

Instead, your entity object can access the root application module in which it finds itself at runtime, and use that application module instances's createViewObject() method to create an instance of the "validation" view object it requires. As with view object instances added to the data model at design time, this API allows you to assign an instance name to the view object so you can use findViewObject() to find it again when needed.

Since the SQL-based validation code may be executed multiple times, it would not be the most efficient approach to create the view object instance each time it's needed and remove it when you are done using it. Instead, you can implement a helper method like what you see in Example 9-7 to use the view object instance if it already exists, or otherwise create it the first time it's needed. In order to ensure that the instance name of the runtime-created view object instance will not clash with the name of any design-time-specified ones in the data model, you can adopt the convention of constructing a name based on the view object definition name, prefixed by a string like "Validation_". This is just one approach. As long as the name doesn't clash with a design time supplied name, you can use any naming scheme.

Example 9-7 Helper Method to Access View Object for Validation

/**
 * Find instance of view object used for validation purposes in the
 * root application module. By convention, the instance of the view
 * object will be named Validation_your_pkg_YourViewObject.
 *
 * If not found, create it for the first time.
 *
 * @return ViewObject to use for validation purposes
 * @param viewObjectDefName
 */
protected ViewObject getValidationVO(String viewObjectDefName) {
  // Create a new name for the VO instance being used for validation
  String name = "Validation_" + viewObjectDefName.replace('.', '_');
  // Try to see if an instance of this name already exists
  ViewObject vo = getDBTransaction().getRootApplicationModule()
                                    .findViewObject(name);
  // If it doesn't already exist, create it using the definition name
  if (vo == null) {
    vo = getDBTransaction().getRootApplicationModule()
                           .createViewObject(name,viewObjectDefName);
  }
  return vo;
}

With a helper method like this in place, your validation code can call getValidationVO() and pass it the fully qualified name of the view object definition that it wants to use. Then you can write code like what you see in Example 9-8.

Example 9-8 Using a Validation View Object in a Method Validator

// Sample entity-level validation method
public boolean validateSomethingUsingViewObject() {
  Number numVal = getSomeEntityAttr();
  String stringVal = getAnotherEntityAttr();
  // Get the view object instance for validation
  ViewObject vo = getValidationVO("devguide.example.SomeViewObjectName");
  // Set it's bind variables (which it will typically have!)
  vo.setNamedBindWhereClauseParam("BindVarOne",numVal);
  vo.setNamedBindWhereClauseParam("BindVarTwo",stringVal);
  vo.executeQuery();
  if ( /* some condition */) {
    /*
     * code here returns true if the validation succeeds
     */
  }
  return false;
}

As the sample code suggests, view objects used for validation will typically have one or more named bind variables in them. Depending on the kind of data your view object retrieves, the "/* some condition */" expression above will look different. For example, if your view object's SQL query is selecting a COUNT() or some other aggregate, the condition will typically use the vo.first() method to access the first row, then use the getAttribute() method to access the attribute value to see what the database returned for the count.

If the validation succeeds or fails based on whether the query has returned zero or one row, the condition might simply test whether vo.first() returns null or not. If vo.first() returns null, there is no "first" row. In other words, the query retrieved no rows.

In other cases, you may be iterating over one or more query results retrieved by the view object to determine whether the validation succeeds or fails.

9.6.2 Implementing an Efficient Existence Check

One common kind of SQL-based validation is a simple test that a candidate foreign key value exists in a related table. This type of validation can be implemented using the findByPrimaryKey() method on the entity definition, however that will retrieve all attributes of the entity if the entity exists. An alternative approach to perform a lighter-weight existing check involves using a view object for validation.

Example 9-9 shows the exists() method that the SRDemo application's Product entity object contains in its custom entity definition class. First, it uses a variant of the helper method above that accepts a DBTransaction as a parameter to return the instance of the appropriate validation view object. This is encapsulated inside the getProductExistsVO() method in the same class.

This read-only view object used for validation is named ProductExistsById in the oracle.srdemo.model.entities.validationqueries package. Since this view object has a custom Java class (ProductExistsByIdImpl), the code in the exists() method can use the strongly-typed setTheProductId() method to set the named bind variable that the view object defines. Then the code executes the query and sets the boolean foundIt variable based on whether a row was found, or not.

Example 9-9 Efficient Existence Check Using View Object and Entity Cache

// In ProductDefImpl.java in SRDemo sample
public boolean exists(DBTransaction t, Number productId) {
  boolean foundIt = false;
  ProductExistsByIdImpl vo = getProductExistsVO(t);
  vo.setTheProductId(productId);
  vo.setForwardOnly(true);
  vo.executeQuery();
  foundIt = (vo.first() != null);
  /*   
   * If we didn't find it in the database,
   * try checking against any new employees in cache
   */
  if (!foundIt) {
    Iterator iter = getAllEntityInstancesIterator(t);
    while (iter.hasNext()) {
      ProductImpl product = (ProductImpl)iter.next();
      /*
       * NOTE: If you allow your primary key attribute to be modifiable
       *       then you should also check for entities with entity state
       *       of Entity.STATUS_MODIFIED here as well.
       */
      if (product.getEntityState() == Entity.STATUS_NEW
          && product.getProdId().equals(productId)) {
        foundIt = true;
        break;
      }
    }
  }  
  return foundIt;
}

Even though the SRDemo application currently does not allow the end user to create new products, it's good to implement the validation in a way that assumes the user might be able to do this in some future screens that are implemented. The code that follows the executeQuery() tests to see whether the candidate product ID is for a new Product entity that exists in the cache.

Recall that since the validation view object has no entity usages, its query will only "see" rows that are currently in the database. So, if the foundIt variable is false after trying the database, the remaining code gets an iterator for the ProductImpl entity rows that are currently in the cache and loops over them to see if any new Product entity row has the candidate product ID. If it does, the exists() method still returns true.

9.6.3 Validating Conditions Related to All Entities of a Given Type

The beforeCommit() method is invoked on each entity row in the pending changes list after the changes have been posted to the database, but before they are committed. This can be a perfect method in which to execute view object-based validations that must assert some rule over all entity rows of a given type.

Note:

If your beforeCommit() logic can throw a ValidationException, you must set the jbo.txn.handleafterpostexc property to true in your configuration to have the framework automatically handle rolling back the in-memory state of the other entity objects that may have already successfully posted to the database (but not yet been committed) during the current commit cycle.

9.7 How to Access Related Entity Rows Using Association Accessors

Often your validation rules or programmatic defaulting of derived values may require consulting the values of associated entity rows. The association accessor methods in your entity object custom Java class make this task extremely easy. By calling the accessor method, you can easily access any related entity row — or RowSet of entity rows — depending on the cardinality of the association.

Example 9-10 shows an example of programmatic defaulting logic in use in the SRDemo application's ServiceHistory entity object. The line number of the new service history row is calculated by accessing the containing parent entity row of type ServiceHistoryImpl, and invoking a helper method called getMaxHistoryLineNumber() on it, before incrementing that value by one. If the parent entity row is already in the cache, the association accessor accesses the row from there. If not, it is brought into the cache using the primary key.

Example 9-10 Accessing Composing Parent Entity Row In a Create Method

// In ServiceHistoryImpl.java in SRDemo sample
protected void create(AttributeList nameValuePair) {
  super.create(nameValuePair);
  setSvhType(getDefaultNoteType());
  setCreatedBy(getCurrentUserId());
  setLineNo(new Number(getServiceRequest().getMaxHistoryLineNumber()+1));
}

Example 9-11 illustrates the code for the getMaxHistoryLineNumber() in the ServiceRequest entity object's custom Java class. It shows another use of an association accessor to retrieve the RowSet of children ServiceHistory rows (of type ServiceHistoryImpl) in order to calculate the maximum value of the LineNo attributes in the existing service history rows.

Example 9-11 Accessing Composed Children Entity Rows in a Calculation Using Association Accessor

// In ServiceRequestImpl.java in SRDemo Sample
public long getMaxHistoryLineNumber() {
  long max = 0;
  RowSet histories = (RowSet)getServiceHistories();
  if (histories != null) {
    while (histories.hasNext()) {
      long curLine = ((ServiceHistoryImpl)histories.next()).getLineNo()
                                                           .longValue();
      if (curLine > max) {
        max = curLine;
      }
    }
  }
  histories.closeRowSet();
  return max;
}

9.8 How to Reference Information About the Authenticated User

If you have set the jbo.security.enforce runtime configuration property to the value Must or Auth, the oracle.jbo.server.SessionImpl object provides methods you can use to get information about the name of the authenticated user and information about the roles of which they are a member. This is the implementation class for the oracle.jbo.Session interface that clients can access.

9.8.1 Referencing Role Information About the Authenticated User

The oracle.jbo.Session interface provides the two methods:

  • String[] getUserRoles(), returns array of role names to which the user belongs

  • boolean isUserInRole(String roleName), returns true if user belongs to specified role

Your entity object code can access the Session by calling:

Session session = getDBTransaction().getSession();

Example 9-12 shows a helper method that uses this technique. It determines whether the current user is a technician by using the isUserInRole() method to test whether the user belongs to the technician role.

Example 9-12 Helper Method to Test Whether Authenticated User is in a Given Role

protected boolean currentUserIsTechnician() {
  return getDBTransaction().getSession().isUserInRole("technician");
}

After refactoring the constants into a separate SRConstants class, the SRDemo application contains helper methods like this in its base SREntityImpl class that all entity objects in the sample extend to inherit this common functionality:

protected boolean currentUserIsTechnician() {
  return getDBTransaction().getSession()
                           .isUserInRole(SRConstants.TECHNICIAN_ROLE);
}
protected boolean currentUserIsManager() {
  return getDBTransaction().getSession()
                           .isUserInRole(SRConstants.MANAGER_ROLE);
}
protected boolean currentUserIsCustomer() {
  return getDBTransaction().getSession()
                           .isUserInRole(SRConstants.USER_ROLE);
}
protected boolean currentUserIsStaffMember() {
  return currentUserIsManager() || currentUserIsTechnician();
}

These are then used by the create() method to conditionally default the service request type based on the role of the current user. The getDefaultNoteType() helper method:

// In ServiceHistoryImpl.java in SRDemo sample
private String getDefaultNoteType() {
  return currentUserIsStaffMember() ? TECHNICIAN_TYPE : CUSTOMER_TYPE;
}

is used by the ServiceHistory entity object's overridden create() method to default the service history type based on the role of the current user.

// In ServiceHistoryImpl.java in SRDemo sample
protected void create(AttributeList nameValuePair) {
  super.create(nameValuePair);
  setSvhType(getDefaultNoteType());
  setCreatedBy(getCurrentUserId());
  setLineNo(new Number(getServiceRequest().getMaxHistoryLineNumber()+1));
}

9.8.2 Referencing the Name of the Authenticated User

In order to access the name of the authenticated user, you need to cast the Session interface to its SessionImpl implementation class. Then you can use the getUserPrincipalName() method. Example 9-13 illustrates a helper method you can use in your entity object to retrieve the current user name.

Example 9-13 Helper Method to Access the Current Authenticated User Name

protected String getCurrentUserName() {
  SessionImpl session = (SessionImpl)getDBTransaction().getSession();
  return session.getUserPrincipalName();
}

9.9 How to Access Original Attribute Values

If an entity attribute's value has been changed in the current transaction, when you call the attribute getter method for it you will get this pending changed value. Using the getPostedAttribute() method, your entity object business logic can consult the original value for any attribute as it was read from the database before the entity row was modified. The method takes the attribute index as an argument, so pass the appropriate generated attribute index constants that JDeveloper maintains for you.

9.10 How to Store Information About the Current User Session

If you need to store information related to the current user session in a way that entity object business logic can reference, you can use the user data hashtable provided by the Session object. Consider how the SRDemo application is using it. When a new user accesses an application module for the first time, the prepareSession() method is called. As shown in Example 9-14, the SRService application module overrides prepareSession() to automatically retrieve information about the authenticated user by calling the retrieveUserInfoForAuthenticatedUser() method on the LoggedInUser view object instance. Then, it calls the setUserIdIntoUserDataHashtable() helper method to save the user's numerical ID into the user data hashtable.

Example 9-14 Overriding prepareSession() to Automatically Query User Information

// In SRServiceImpl.java in SRDemo Sample
protected void prepareSession(Session session) {
  super.prepareSession(session);
  /*
   * Automatically query up the correct row in the LoggedInUser VO
   * based on the currently logged-in user, using a custom method
   * on the LoggedInUser view object component.
   */
  getLoggedInUser().retrieveUserInfoForAuthenticatedUser();     
  setUserIdIntoUserDataHashtable();
}

Example 9-15 shows the code for the LoggedInUser view object's retrieveUserInfoForAuthenticatedUser() method. It sets its own EmailAddress bind variable to the name of the authenticated user from the session and then calls executeQuery() to retrieve the additional user information from the USERS table.

Example 9-15 Accessing Authenticated User Name to Retrieve Additional User Details

// In LoggedInUserImpl.java
public void retrieveUserInfoForAuthenticatedUser() {
  SessionImpl session = (SessionImpl)getDBTransaction().getSession();
  setEmailAddress(session.getUserPrincipalName());
  executeQuery();
  first();
}

One of the pieces of information about the authenticated user that the LoggedInUser view object retrieves is the user's numerical ID number, which that method returns as its result. For example, the user sking has the numeric UserId of 300.

Example 9-16 shows the setUserIdIntoUserDataHashtable() helper method — used by the prepareSession() code above — that stores this numerical user ID in the user data hashtable, using the key provided by the string constant SRConstants.CURRENT_USER_ID.

Example 9-16 Setting Information into the UserData Hashtable for Access By Entity Objects

// In SRServiceImpl.java
private void setUserIdIntoUserDataHashtable() {
  Integer userid = getUserIdForLoggedInUser();
  Hashtable userdata = getDBTransaction().getSession().getUserData();
  if (userdata == null) {
    userdata = new Hashtable();
  }
  userdata.put(SRConstants.CURRENT_USER_ID, userid);
}  

Both the ServiceRequest and the ServiceHistory entity objects have an overridden create() method that references this numerical user ID using a helper method like the following to set the CreatedBy attribute programmatically to the value of the currently authenticated user's numerical user ID.

protected Number getCurrentUserId() {
  Hashtable userdata = getDBTransaction().getSession().getUserData();
  Integer userId = (Integer)userdata.get(SRConstants.CURRENT_USER_ID);
  return userdata != null ? Utils.intToNumber(userId):null;
}

9.11 How to Access the Current Date and Time

You might find it useful to reference the current date and time in your entity object business logic. Example 9-17 shows a helper method you can use to access the current date without any time information.

Example 9-17 Helper Method to Access the Current Date with No Time

/*
 * Helper method to return current date without time
 *
 * Requires import: oracle.jbo.domain.Date
 */
protected Date getCurrentDate() {
  return new Date(Date.getCurrentDate());
}

In contrast, if you need the information about the current time included as part of the current date, use the helper method shown in Example 9-18.

Example 9-18 Helper Method to Access the Current Date with Time

/*
 * Helper method to return current date with time
 *
 * Requires imports: oracle.jbo.domain.Date
 *                   java.sql.Timestamp
 */
protected Date getCurrentDateWithTime() {
  return new Date(new Timestamp(System.currentTimeMillis()));    
}

9.12 How to Send Notifications Upon a Successful Commit

The afterCommit() method is invoked on each entity row that was in the pending changes list and got successfully saved to the database. You might use this method to send an email notification about the change in state of an entity.

9.13 How to Conditionally Prevent an Entity Row from Being Removed

The remove() method is invoked on an entity row before it is removed. You can conditionally throw a JboException in this method to prevent a row from being removed if the appropriate conditions are not met.

Note:

The entity object offers declarative prevention of deleting a master entity row that has existing, composed children rows. You configure this option on the Association Properties page of the Association Editor for the composition.

9.14 How to Implement Conditional Updatability for Attributes

You can override the isAttributeUpdateable() method in your entity object class to programmatically determine whether a given attribute is updatable or not at runtime based on appropriate conditions. Example 9-19 shows how the ServiceHistory entity object in the SRDemo application overrides this method to enforce that its SvhType attribute is updatable only if the current authenticated user is a staff member. Notice that when the entity object fires this method, it passes in the integer attribute index whose updatability is being considered. You implement your conditional updatability logic for a particular attribute inside an if or switch statement based on the attribute index. Here SVHTYPE is referencing the integer attribute index constants that JDeveloper automatically maintains in your entity object custom Java class.

Example 9-19 Conditionally Determining an Attribute's Updatability at Runtime

// In ServiceHistoryImpl.java
public boolean isAttributeUpdateable(int index) {
  if (index == SVHTYPE) {
    if (!currentUserIsStaffMember()) {
      return super.isAttributeUpdateable(index);
    }
    return CUSTOMER_TYPE.equals(getSvhType()) ? false : true;
  }
  return super.isAttributeUpdateable(index);
}

Note:

Entity-based view objects inherit this conditional updatability as they do everything else encapsulated in your entity objects. Should you need to implement this type of conditional updatability logic in a way that is specific to a transient view object attribute, or to enforce some condition that involves data from multiple entity objects participating in the view object, you can override this same method in a view object's view row class to achieve the desired result.

9.15 Additional Resources

The Business Rules in ADF Business Components whitepaper by Oracle Consulting outlines a formal approach to classifying and implementing virtually every kind of real-world business rule they have encountered in their project implementations using Oracle ADF using ADF Business Components. You can access it from the Oracle JHeadstart Product Center on OTN.