Maintenance documents rely on the fact that most behavior can be configured in the data dictionary. But what if your document behaves in ways that don't quite conform to data dictionary defaults? Are you forced to write a transactional document? Not at all...we've got Maintainables coming to our rescue.
- What are Maintainables?
- Basic Maintainable behavior
- Generating different locks
- Document event callbacks
- Collections methods
- Changing the maintenance document's UI
What are Maintainables?
We earlier met maintainables in the section on business rules for maintenance documents. We saw that maintenance documents hand the rules a document of class org.kuali.core.document.MaintenanceDocument. This document held the new business object and perhaps the pre-edit or pre-copy version of the business object...but that those business objects were wrapped in other objects that implemented the org.kuali.core.maintenance.Maintainable interface.
Most of the time business objects in maintenance documents are simply wrapped in the generic org.kuali.core.maintenance.KualiMaintainableImpl implementation of Maintainable. But that doesn't mean a business object has to use that implementation of Maintainable....
Every maintenance document data dictionary configuration must include the class of the Maintainable business object wrapper the maintenance document will use. Most maintenance documents specify this:
But if we look at the Account Maintenance Document data dictionary file, in org/kuali/module/chart/datadictionary/docs, we see this as the Maintainable:
That's right: the Account Maintenance Document defines its own Maintainable implementation. (Gasp! The Horror!)
Why would we do this for a maintenance document? Much of what the Maintainable does can be derived directly from the data dictionary file for the maintenance document. However, creating our own Maintainable gives us a lot more power: basically, we can have dynamic, code-driven solutions, instead of static, XML-driven solutions. They're used mainly in situations where the maintenance document needs some extra configuration that the data dictionary can't provide. The main uses are in generating different document locks for business objects, having more control over document callbacks like transactional documents do, and even overriding the UI for a document. They also hold collections methods, so it's worthwhile to take a look at those too. We'll start our tour with some of the basic information held in the maintainable.
Basic Maintainable behavior
Here's a quick (and by no means comprehensive) sample of some of the basic things that a Maintainable instance does.
- getBusinessObject() - the most basic method of the Maintainable, which returns the maintained business object itself. A related method is getBOClass(), which is typically populated directly from the data dictionary.
- populateBusinessObject() - given a Map of key/value pairs, this method populates the business object, using the key names as properties to set. This behavior can be overridden, which opens a lot of interesting possibilities.
- getDocumentTitle() - given a MaintenanceDocument, returns the document title
- refresh() - this requests a refresh of the business object; it's called after a business object is returned from a Lookup operation
- getMaintenanceAction() and setMaintenanceAction() - it is definitely NOT suggested that these methods be overriden. Basically, this getter/setter pair deals with what action is going on in the maintenance document, whether it is a new creation, an edit, a copy, or a setup with existing
- saveBusinessObject() - Most trivial business objects won't have a need to override this method, which in KualiMaintainableImpl defers to the BusinessObjectService to save the BO. However, it is overridden by KualiGlobalMaintainableImpl, so that the new business objects created by the GlobalBusinessObjects all get saved, deleted, or updated, and by AccountMaintainableImpl, which has to process certain deactivations after the account was saved (and therefore, could not perform its logic in a callback).
Since much of this functionality is basic, most Maintainables extend KualiMaintainableImpl so that they can simply inherit this behavior and override only what they need to.
A note before we continue about the maintenanceAction property. There are four possible actions, and at least three of them are named in obvious ways; they're all constants in RiceConstants. They are MAINTENANCE_NEW_ACTION, MAINTENANCE_COPY_ACTION, MAINTENANCE_EDIT_ACTION, and MAINTENANCE_NEWWITHEXISTING_ACTION. The new action is for creating a new business object, the copy action is for copying an old business object, and the edit action is for editing a business object. That leaves us "new with existing," which sounds suspiciously like copy but which is not, in fact, copy. The new with existing action is only used in a couple of places in KFS, including certain Vendor-based maintenance documents in Purchasing & Accounts Payable, and in the "global account delegate templates." Basically, the action is used for cases where a lookup is creating a new business object from heterogeneous business object - for instance, when a parent business object creates a child business object. Not much used in KFS, again, but an interesting feature.
Generating different locks
The major use of maintainables is to generate different locks for the business objects under maintenance than would typically be generated. Maintenance locks are requested with the List<MaintenanceLock> generateMaintenanceLocks() method.
A MaintenanceLock object is basically a document number and a locking representation of the business object being maintained, which typically has the class name of the business object and then the names and values of each of the fields listed as locking keys in the maintenance document's data dictionary. Here's a locking key for an object code:
Yeah, kind of yucky looking...that's why we're happy that KualiMaintainableImpl typically does the work for us. However, there are occasions when a business object may need to create different locking codes based on its state. For instance, if an Account Delegate is a primary route delegate, then it needs to have the lockingKeys maintenance lock created, but also a second lock to prevent other account delegates from becoming primary route delegates on the same account. Therefore, that maintenance document defines its own extension of KualiMaintainableImpl, org.kuali.module.chart.maintenance.KualiDelegateMaintainableImpl, which overrides the getMaintenanceLocks() method as so:
As we can see, this calls the super.generateMaintenanceLocks() to get the locks the data dictionary would typically demand, and then if the delegate is a primary route delegate, calls a private method to create a new lock, based on different fields of the business object.
Document event callbacks
In most frameworks, there are hooks that get called when certain events occur within the framework. For instance, we might want to have some code executed before a business object is saved. The maintenance framework has several such callbacks, so let's take a look at them.
- prepareForSave() - Well, here we go. As we've all guessed, this allows the business object to execute some code before it saves. If, for instance, a business object needed a related header business object, that header could be created and related to the business object in this method.
- processAfterRetrieve() - this method is called after a business object has been retrieved from the persistence store.
- processAfterCopy() - this code is executed on a business object that is the product of a copy, which prepares the copy for user editing ("processAfterCopy()" is snappier than "prepareCopiedBusinessObjectForUserInteractionAndSubsequentEditing()", though). UniversalUserMaintainableImpl calls this method, for instance, to replace the personUserIdentifier field with a blank String.
- processAfterEdit() - this code is executed on a business object before the user sees it on an edit maintenance document action.
- processAfterAddLine() - this method is called after a new collection line is added to the maintainable.
- handleRouteStatusChange() - this method is the same as transactional documents have. It's basically a callback for KEW to tell the maintainable what workflow route the document is in. For instance, once a maintenance documents goes into "Final" status, a business object may need to create some child business objects. This callback would allow the Maintainable, and therefore, the business object, a way to know the document went into "Final" status in workflow.
- setupNewFromExisting() - this allows a new business object to prepare itself for user editing for a setup new from existing action as described above.
In all of these cases, it's typically a wise idea to call the super version of the callback. When we look through KualiMaintainableImpl, we see that there is no default implementation of most of these methods. However, there are implementations of some of the methods, such as processAfterCopy(), so calling the super version makes it easier to keep things clear. Furthermore, future versions of KualiMaintainableImpl may add default behavior to these callbacks.
We covered creating collections back at the data dictionary. Collections are handled by three methods in Maintainable. These will rarely need to be overridden, but it's still likely a good idea to explore them quickly:
- getNewCollectionLine() - This is handed a collection name String as a parameter. As we remember, each collection on a maintenance document has a unique name, which maps to the name of a property in the maintenance document which returns a List. The parameter, therefore, tells the Maintainable which collection a new line should be created for. It returns an empty business object to be the new "line" in the collection
- addNewLineToCollection() - Again, this takes, as a parameter, the name of the collection, and it adds the new collection line created by getNewCollectionLine() and adds it to the proper collection in the business object being maintained.
- populateNewCollectionLines() - this takes in a Map of values and appropriately populates a new collection line and any sub collections associated with the line. It then returns a Map of values that could not be properly formatted to be populated in the new business object.
These three methods are the support the Maintainable gives to the collection framework.
Changing the maintenance document's UI
Most of the time, the form for a maintenance document is created entirely from the data dictionary. Most of the time. However, there are also cases when a Maintainable will want to dynamically change the form for the maintenance document. Three uses of this exist in KFS: the Universal User maintenance document adds sections based on the modules the user belongs to; the Proposal maintenance document hides sections if users creating the document don't have privileges to enter certain information; and the Vendor maintenance document doesn't allow adding to a certain collection unless the user is in a certain workgroup. Yep, this is pretty obtuse stuff.
The method to change the sections is getSections(). It is handed, as a parameter, the old Maintainable object for a given document, and it returns a List of org.kuali.core.web.ui.Section objects. These are the same objects that are created automatically by the document's data dictionary configuration.
Most implementations of getSections() get the sections from the old Maintainable which was passed in as a parameter and then adds and subtracts sections as necessary. It's a lot of work, but in this method, one has total control over building the UI for a given maintenance document. If that seems like an interesting thing to you, then you are urged to look at the Maintainable implementations for the maintenance documents mentioned above.