Oracle® Application Development Framework Developer's Guide For Forms/4GL Developers 10g Release 3 (10.1.3.0) Part Number B25947-02 |
|
|
View PDF |
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:
Section 9.4, "Assigning Programmatically-Derived Attribute Values"
Section 9.5, "Undoing Pending Changes to an Entity Using the Refresh Method"
Section 9.7, "How to Access Related Entity Rows Using Association Accessors"
Section 9.8, "How to Reference Information About the Authenticated User"
Section 9.10, "How to Store Information About the Current User Session"
Section 9.12, "How to Send Notifications Upon a Successful Commit"
Section 9.13, "How to Conditionally Prevent an Entity Row from Being Removed"
Section 9.14, "How to Implement Conditional Updatability for Attributes"
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:
Attribute-level method validators trigger validation code when an attribute value changes.
Entity-level method validators trigger validation code when an entity row is validated.
You can override the following key methods in a custom Java class for an entity:
create()
, to assign default values when a row is created
initDefaults()
, to assign defaults either when a row is created or when a new row is refreshed
isAttributeUpdateable()
, to make attributes conditionally updatable
remove()
, to conditionally disallow deleting
prepareForDML()
, to assign attribute values before an entity row is saved
beforeCommit()
, to enforce rules that must consider all entity rows of a given type
afterCommit()
, to send notifications about a change to an entity object's state
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.
Entity object validation rules fall into two basic categories: attribute-level and entity-level.
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.
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.
Transaction commit processing happens in three basic phases:
Ensure any invalid entity rows on the pending changes list are valid.
Post the pending changes to the database by performing appropriate DML operations.
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-levelpostChanges()
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.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.
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.
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.
You can use the getEntityState()
method to access the current state of an entity row in your business logic code.
Note:
If you use thepostChanges()
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.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.
To create an attribute-level method validator:
Open the Entity Object Editor
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.
Open the Validation page and select the attribute that you want to validate.
Click New to add a validation rule.
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.
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 thecompareTo()
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.To create an entity-level method validator:
Open the Entity Object Editor.
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.
Open the Validation page and select root node in the tree that represents the entity object itself.
Click New to add a validation rule.
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.
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
.
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.
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.
When declarative defaulting falls short of your needs, you can perform programmatic defaulting in your entity object:
When an entity row is first created
When the entity row is first created or when refreshed to blank again
When the entity row is saved to the database
When an entity attribute value is set
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)); }
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.
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()); }
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(); } } }
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.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.
REFRESH_WITH_DB_FORGET_CHANGES
forgets modifications made to the row in the current transaction and the row's data is refreshed from database. The latest data from database replaces data in the row regardless of whether the row was modified or not.
REFRESH_WITH_DB_ONLY_IF_UNCHANGED
works just like REFRESH_WITH_DB_FORGET_CHANGES
, but for unmodified rows. If a row was already modified by this transaction, the row is not refreshed.
REFRESH_UNDO_CHANGES
works the same as REFRESH_WITH_DB_FORGET_CHANGES
for unmodified rows. For a modified row, this mode refreshes the row with attribute values at the beginning of this transaction. The row remains in a modified state.
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
.
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.
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.
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.
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
.
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 yourbeforeCommit()
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.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; }
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.
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)); }
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.
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.
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; }
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.
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.
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.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.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.