When actions are performed on documents, there is typically some validation to accomplish on the document; indeed, a great deal of the business logic for client application is stored in document validations. The KNS supports a standard framework for validations as well as a way to display errors to application end users.
KNS validations are performed by rules classes, which respond to a specific application event. An event is an object which encapsulates contextual information about something which has been requested of a document. For instance, when a user on a maintenance document clicks a “Route” button to route the document into workflow, the web-layer controller creates an instance of org.kuali.rice.kns.rule.event.RouteDocumentEvent which holds the document which has just been routed. It then passes this event instance to org.kuali.rice.kns.service.KualiRuleService.
The KualiRuleService interrogates the data dictionary entry for the document to find a rules class. The event then invokes the rules class against itself. This is accomplished through a rule interface. Every event has an associated rule interface; the class of this interface is returned by the Event’s getRuleInterfaceClass() method. The event will cast the business rule from the data dictionary to the interface which it expects, and then call a standard method against that interface.
An example will clarify this. RouteDocumentEvent expects rules implementing the rule interface org.kuali.rice.kns.rule.RouteDocumentRule, which extends the BusinessRule interface given above. RouteDocumentRule has a single method to implement:
public boolean processRouteDocument(Document document);
When the KualiRuleService gets the event, it finds the data dictionary entry for the given document and generates an instance of the business rules class associated with the document. It then hands that to the event, which attempts to perform the cast to RouteDocumentRule and call the processRouteDocument method:
public boolean invokeRuleMethod(BusinessRule rule) { return ((RouteDocumentRule) rule).processRouteDocument(document); }
It then returns whatever was returned by the rule.
This brings up the question of what the processRouteDocument method should actually do. Rule methods need to accomplish two things:
Run the business logic associated with that event against the document. If the business logic decides the document is valid, then a true should be returned. If the business logic, contrarily, decides the document is not valid, a false is typically returned. The result of the method invocation then typically determines whether the given event will be completed. For instance, if processRouteDocument returns a false, then the document – which has only had a workflow route requested of it – will fail to route. It will instead return to the document screen.
Some kind of user message should be recorded in the GlobalVariables.getMessages() thread-local singleton. This singleton has three maps, accessible through the getErrorMap(), getWarningMap(), and getInfoMap() methods. These maps associate an attribute on the page which caused a failure with a user message explaining the problem. If a false is returned from the method, then it is generally expected that the failure will be recorded in the Error map.
An excellent example of this can be found in the sample “Recipe application” which ships with Rice, in edu.sampleu.recipe.document.rule.RecipeRules:
@Override protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) { boolean valid = super.processCustomSaveDocumentBusinessRules(document); if (valid) { valid &= validateIngredients(document); } return valid; } private boolean validateIngredients(MaintenanceDocument recipeDocument) { Recipe recipe = (Recipe) recipeDocument.getDocumentBusinessObject(); String ingredients = recipe.getIngredients(); RecipeCategory category = recipe.getCategory(); if (category != null) { String categoryName = recipe.getCategory().getName(); if(StringUtils.containsIgnoreCase(ingredients, "beef") && !StringUtils.equalsIgnoreCase(categoryName, "beef")) { putFieldError("categoryId", "error.document.maintenance.recipe.ingredients.beef"); return false; } } return true; }
In this example, the processCustomSaveDocumentBusinessRules is called when the document is saved. In turn, the validateIngredients method is called. It checks that if the category is not null, then if “beef” is among the ingredients, then the categoryName of the recipe must include the word “beef” in it. If that is the case, we see that the putFieldError – a convenience method – adds the user message to the “categoryId” attribute (meaning the error message will be displayed close to that attribute) and that false is returned, meaning that the save is not carried out.
There are eight common KNS events which apply to every document – maintenance and transactional – built within client applications. For each, the KNS does an amount of standard validation, while leaving customization points so client applications can add more validation business logic. They are:
Table 5.3. KNS Events
Event | Calling circumstances | Rule interface and method called | Validation performed in DocumentRuleBase |
---|---|---|---|
org.kuali.rice.kns.rule.event.RouteDocumentEvent | Called when a document is routed to workflow. | org.kuali.rice.kns.rule.RouteDocumentRule#processRouteDocument | Performs standard data dictionary validation |
org.kuali.rice.kns.rule.event.SaveDocumentEvent | Called when a document is saved. | org.kuali.rice.kns.rule.SaveDocumentRule#processSaveDocument | Performs standard data dictionary validation |
org.kuali.rice.kns.rule.event.ApproveDocumentEvent | Called when a workflow action is taken against a document. | org.kuali.rice.kns.rule.ApproveDocumentRule#processApproveDocument | |
org.kuali.rice.kns.rule.event.BlanketApproveDocumentEvent | Called when a document is blanket approved through workflow. | org.kuali.rice.kns.rule.ApproveDocumentRule#processApproveDocument | |
org.kuali.rice.kns.rule.event.AddNoteEvent | Called when a note is added to a document. | org.kuali.rice.kns.rule.AddNoteRule#processAddNote | Validates the note (via the data dictionary) |
org.kuali.rice.kns.rule.event.AddAdHocRoutePersonEvent | Called when an ad hoc Person to route to is added to a document. | org.kuali.rice.kns.rule.AddAdHocRoutePersonRule#processAdHocRoutePerson | Validates that the ad hoc route Person is valid – that the Person’s record exists and that the Person has the permission to approve the document |
org.kuali.rice.kns.rule.event.AddAdHocRouteWorkgroupEvent | Called when an Ad Hoc workgroup to route to is added to a document. | org.kuali.rice.kns.rule.AddAdHocRouteWorkgroupRoute#processAddAdHocRouteWorkgroup | Validates the ad hoc route workgroup – that the workgroup exists and that the workgroup has permission to receive an ad hoc request and approve the document. |
org.kuali.rice.kns.rule.event.SendAdHocRequestsEvent | Called when the end user requests that ad hoc events be sent. |
Since the standard events have to perform standard validation, they have custom methods to override. For instance, org.kuali.rice.kns.rules.DocumentRuleBase has a method “processCustomRouteDocumentBusinessRules” and it is expected that client applications will override this method rather than processRouteDocumentBusinessRules directly.
Maintenance documents add another event to this: org.kuali.rice.kns.rule.event.KualiAddLineEvent. This is invoked when a new item is added to a collection on a maintenance document. The org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase also contains a number of useful utility methods which makes writing business rules for maintenance documents easier.
When a validation results in some kind of text being displayed to the user, GlobalVariables.getMessageMap() is used to store that text and is inquired during rendering to make sure messages are correctly displayed. As mentioned previously, the MessageMap is made up of three different maps: one for errors, one for warnings, and one for information messages. Each map has a “put” command – for instance, putError; each has a “has” predicate, such as “hasErrors”; and each have the ability to get the properties with form the keys of the map as well as any messages associated with that property. Adding, an error message to the map is easy, as seen in this example from the IdentityManagementGroupDocument:
GlobalVariables.getMessageMap().putError("document.member.memberId", RiceKeyConstants.ERROR_EMPTY_ENTRY, new String[] {"Member Type Code and Member ID"});
The method takes the property that the error is most associated with, which determines where the text will be displayed (ie, at the top of the section which contains the given property); a key to the User Message containing the error; and an array of Strings which will be interpolated into the message using the standard Java java.text.MessageFormat.
Further details about the use of User Messages can be found in the KNS User Messages section.
While the vast majority of maintenance documents in client applications will not have custom actions, it is common in transactional documents to have new events beyond the standard ones provided by the KNS framework. Basically, any button created on a transactional document – one which results in a call to a method in the transactional document’s action class – may well have an event associated with it. In that case, there are three pieces to create for the rule: the new event, the rule instance which is called from that event, and the default implementation for that rule.
An example from Kuali Financial Systems 3.0 will illustrate how these are used. The Cash Control transactional document in the Accounts Receivable module has a collection of details, added via an “add” button. To validate that action, an event was created (this code has slightly been altered for the sake of illustration):
package org.kuali.kfs.module.ar.document.validation.event; public final class AddCashControlDetailEvent extends KualiDocumentEventBase { private final CashControlDetail cashControlDetail; public AddCashControlDetailEvent(String errorPathPrefix, Document document, CashControlDetail cashControlDetail) { super("Adding cash control detail to document " + getDocumentId(document), errorPathPrefix, document); this.cashControlDetail = cashControlDetail; } public Class getRuleInterfaceClass() { return AddCashControlDetailRule.class; } public boolean invokeRuleMethod(BusinessRule rule) { return ((AddCashControlDetailRule) rule).processAddCashControlDetailBusinessRules((TransactionalDocument) getDocument(), this.cashControlDetail); } }
The AddCashControlDetailEvent extends the KualiDocumentEventBase class, defined in the KNS. Note that it encapsulates the state to check – both the document at hand and the cash control detail which is being validated. Finally, it implements the two methods which make the rule work: the getRuleInterfaceClass() and the invokeRuleMethod(). This works precisely as it does in the KNS RouteDocumentEvent.
The AddCashControlDetailRule looks like this:
public interface AddCashControlDetailRule<F extends TransactionalDocument > { public boolean processAddCashControlDetailBusinessRules(F transactionalDocument, CashControlDetail cashControlDetail); }
This is very straightforward. There is a rules class, in turn, which implements this interface. Finally, the rules have to be called; that occurs when an event is created and sent to the KualiRuleService, which is typically done in the web layer’s controller. In our example, this occurs in the CashControlDocumentAction:
// apply rules for the new cash control detail rulePassed &= ruleService.applyRules(new AddCashControlDetailEvent(ArConstants.NEW_CASH_CONTROL_DETAIL_ERROR_PATH_PREFIX, cashControlDocument, newCashControlDetail));
Now the new action will be validated properly.