Logging
Kuali defaults logging to the INFO
level. This means that, in production, any messages at INFO level or higher will go into the application logs. Since this is the primary means of monitoring system operation, we need to be careful to not overload these logs with too much detail, as that tends to obscure any problems which may occur. (It may also cause critical troubleshooting information to be rotated out of the logs too soon.)
If it's for development purposes, put any logging at DEBUG or TRACE levels. Logging levels can always be increased as needed, even on production servers. But when logging anything which is not a problem, be sure that the only code which executes when the logging will not be displayed is the check itself. That is, don't assemble the message unless you are going to output it.
Remember that in a call like:
LOG.debug( "Checking contents of array: " + Arrays.asList( someArray ) );
That Java has to first run the Arrays.asList()
method, convert it to a string, build a StringBuilder, append the strings, and re-convert the result into a String. Then, it can pass it to the LOG.debug()
method which decides whether to show the result. Instead, use a form like the below, as it skips the assembly of the message unless it knows it will be displaying it. (Yes, the logging level check is done twice, but that is acceptable since it will only be done twice if DEBUG logging is turned on.
if ( LOG.isDebugEnabled() ) { LOG.debug( "Checking contents of array: " + Arrays.asList( someArray ) ); }
If you are only displaying a static string or a single string variable, no extra check is necessary, as no processing has to be done to pass that into the method.
When declaring a Logger:
- declare the Logger based on an implementation class not it's interface
- explicitly specify the class, don't use getClass() when calling getLogger()
For example:
private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BankServiceImpl.class);
Exception Handling
Java defines two kinds of exceptions: checked exceptions and unchecked exceptions.
- In general, we want to always use unchecked exceptions. This would be all exceptions which extend from RuntimeException.
- Checked exceptions cause a number of headaches.
An exception should be caught and handled explicitly in the following cases:
- If you, under normal operation of the system, know that exceptions could occur in the underlying code AND there is some form of reasonable handling which you can do to mitigate the error.
- If the code shouldn't be allowed to continue, which is true in most cases where exceptions get thrown, either don't catch the exception (if it's a RuntimeException), or repackage the exception as a RuntimeException and rethrow it.
Multi-catch
Java 7 introduced a great new feature where, if multiple exceptions are possibly thrown by a single piece of code and if the catch handles each of those exceptions in the very same way, you can simply have one exception handler, like this:
try { String.class.newInstance(); } catch (IllegalArgumentException | NoSuchMethodException ex) { // i know these aren't right, but you surely get the use of the language feature now, right? LOG.error(ex); throw new RuntimeException(ex); }
Suggestions:
- Every exception should be logged. Don’t silently swallow it.
- Don’t discard any exception information, but add more information to an exception. That is, even if you only catch and re-throw, add some of the relevant method parameters or instance variables to the error message.
Method Visibility
Unless child classes really should not be modifying a variable, make all member variables and methods of a class protected. Kuali applications are often extended by implementors and private member variables make that difficult.
Inject All Services
All services used by another service (or any class which is instantiated via Spring) should be injected via the Spring XML files. This forms a kind of documentation as to what services are being used by the service. These service references should be stored in instance variables of the class.
Of course, if the class (such as a document) is not generated by Spring, then the services must be obtained via the SpringContext.getBean()
method. However, references to these services should be stored in static class variables and retrieved by code via getter methods like the following. This performs the lookup as late as possible and only if needed as well as prevents excessive service lookups.
// this variable is private to ensure that subclasses only use the getter private static DataDictionaryService dataDictionaryService; // This method is protected. Nothing but this class and subclasses should ever obtain the service reference from here. protected DataDictionaryService getDataDictionaryService() { if ( dataDictionaryService == null ) { dataDictionaryService = SpringContext.getBean(DataDictionaryService.class); } return dataDictionaryService; }
Service Injection Exceptions
There are a couple exceptions to this rule.
- Some services/beans have
scope="prototype"
in their definition. A new instance of these beans are created every time it is retrieved. These beans should never be cached, as they, in most cases, store session-specific data. - KEW, KSB, and KIM services should be obtained upon need. These services maybe accessed from a remote Rice server at some point, so we should not assume we can hold a static reference to a remote service, since such a reference is bound to a particular physical node.
- However, if the service will be needed more than once within a single web request, hold onto the reference for the life of the method or request so that you do not incur the cost of re-retrieving the service.
- KIM Exception: the PersonService will always be running locally. It can be injected and/or stored statically.
Do Not Access Services in Static Initializers or Constructors
When classes are instantiated, it can be tempting to initialize service references upon class load or instance creation. Both of these must be avoided. We have no control over when classes are loaded and we do not know when classes may be instantiated. In many cases, these actions take place before the application has completed startup. Accessing the service bus at that time will often result in an error and cause the application startup to fail. (Most business objects and document classes are instantiated during data dictionary validation, so this is a real potential problem. Instead, use the model above to only obtain the service/bean reference upon first demand.
Third-Party Libraries
There are many excellent Java Libraries available which perform operations which are often needed by software applications. Many of the low-level issues we encounter as programmers have already been dealt with, debugged, and used in production applications. We should not re-invent these operations. If you are attempting to perform what seems like a low-level manipulation of data or network operation, please look at the existing libraries in the project, especially the Apache Commons libraries.
Apache Commons libraries
As previously mentioned, the Apache Commons libraries contains classes with a lot of good low-level methods that can be used without developers having to "re-invent the wheel" or use less "safe" alternatives. Here are a couple good examples:
- Use StringUtils.equals() for String comparison since it is null safe. (NOTE: Since methods like StringUtils.equals() and StringUtils.isNotBlank() are null save, they don’t need to be preceded by an ObjectUtils.isNotNull() check.)
- Use FileUtils.forceMkdir() for creating directories.
Adding New Libraries
All Java libraries delivered with Kuali Project must conform to Open Source licenses in order to avoid legal issues. Before a new third party library is introduced, its license should be submitted and evaluated by the Project. Here is the basic evaluation procedure:
Kuali-Specific Coding Standards
Suggestions:
- Try to use the existing and well-known Java libraries to increase development productivity;
- Survey its license before introducing a new third party Java library;
- Don’t put a third party Java library into the Project prior to evaluation and approval from the Project.
KFS Constant Classes
One of the Kuali-Specific Coding Standards for KFS is "Use constants instead of string literals." A developer might ask, "Where is the best place to put my constant?" There are a variety of contants classes in KFS both global to KFS and specific to individual modules. Put your constant in the most fine-grained location possible that is specific to the type of constant and the module that will be using it. For example, an error key constant used by multiple modules would go in the global org.kuali.kfs.sys.KFSKeyConstants
class, while an error key constant that was specific to the PurAP module would go in the org.kuali.kfs.module.purap.PurapKeyConstants
class.
Examples of Constant Classes:
Constant Type | Global KFS Class | Module-Specific class example | Description/Usage |
---|---|---|---|
Constants | org.kuali.kfs.sys.KFSConstants | org.kuali.kfs.module.purap.PurapConstants | Used to define general constants (don't fit in one of the categories below) for the system or module. |
KeyConstants | org.kuali.kfs.sys.KFSKeyConstants | org.kuali.kfs.module.purap.PurapKeyConstants | Holds error key constants that are global or module-specific. |
PropertyConstants | org.kuali.kfs.sys.KFSPropertyConstants | org.kuali.kfs.module.purap.PurapPropertyConstants | Holds property name constants that are global or module-specific. |
ParameterConstants | org.kuali.kfs.sys.KFSParameterKeyConstants | org.kuali.kfs.module.purap.PurapParameterConstants | Has names of parameters associated globally or with a module (inner classes can be used to organize those parameters which don’t fit with the big four components of All, Batch, Document, and Lookup). |
WorkflowConstants | org.kuali.kfs.module.purap.PurapWorkflowConstants | Has node names and application document statuses (inner classes can be used here to differentiate between nodes and statuses used on different documents, like what PurapWorkflowConstants does). | |
AuthorizationConstants | org.kuali.kfs.sys.KfsAuthorizationConstants | org.kuali.kfs.module.purap.PurapAuthorizationConstants | Contains names of edit modes (again, inner classes to separate by document type is fine) and special permission names which need to be called...and in very very rare cases, role names. |
KimAttributes | org.kuali.kfs.sys.identity.KfsKimAttributes | org.kuali.kfs.module.purap.identity.PurapKimAttributes | Special case that holdes constants for KIM attributes as well as instance variables, setters and getters for those attributes. |
KFS Naming Standards
These are examples of naming convention for KFS documents.
BO or Transactional Document Class | BO DD Entry | Doc DD Entry | Workflow Doc Name | Doc Label / Workflow Doc Label / Portal Link |
---|---|---|---|---|
AssetAcquisitionType.java | AssetAcquisitionType.xml | AssetAcquisitionTypeMaintenanceDocument.xml | AssetAcquisitionTypeMaintenanceDocument | Asset Acquisition Type |
AssetDepreciationMethod.java | AssetDepreciationMethod.xml | AssetDepreciationMethodMaintenanceDocument.xml | AssetDepreciationMethodMaintenanceDocument | Asset Depreciation Method |
AssetObjectCode.java | AssetObjectCode.xml | AssetObjectCodeMaintenanceDocument.xml | AssetObjectCodeMaintenanceDocument | Asset Object Code |
Modularization
See also Kuali-Specific Coding Standards
Development Standards
These standards are concerned with ensuring the smoothest sailing possible for implementers who need to switch out one of our optional module implementations. All but Chart, Financial Processing, General Ledger, Pre-Disbursement Processing, and Vendor are optional modules.
For those modules that are not part of the core financial system, the following should be developed to account for java dependencies
A module service interface in the
org.kuali.module.integration.<your module code>
package that contains all methods that the core financial system and other optional modules need to call. This includes data access methods, business rules, document spawning etc. The implementation for this interface will live in theorg.kuali.module.<your module>.service.impl
. The corresponding bean should be defined in theorg.kuali.module.integration.SpringBeansModules.xml
file.Module service bean definitions that reside in
org.kuali.module.integration.SpringBeansModules.xml
cannot reference beans defined in optional module Spring files, e.g. withinLaborModuleServiceImpl
, other Labor services that are relied on need to be obtained fromSpringContext
NOT dependency injection.- Interfaces in the
org.kuali.module.integration<your module>.bo
package for business objects that the core financial system and other optional modules need to deal with as parameters or return values from your module service methods or reference from their lookups, documents, etc. See Externalizable Business Objects for documentation and the following examples: Account BO's references to ContractsAndGrantsCfda and ContractsAndGrantsAccountAwardInformation and the FP CAB data collection code - look for code references to CapitalAssetInformation and capitalAssetInfo.tag, e.g. refs to CapitalAssetManagementAsset. - Public and protected method signatures should not be changed in a non-impacting release. If a method signature change is required, the original method should be deprecated and a new method created. If possible, the original method should call the new method with sensible defaults.
When one module needs to display information from another module, Externalizable Business Objects should be used. When a module needs to collect additional information for another module, Externalizable Business Objects should be used to determine how to draw the input fields, and the depended on module service should be responsible for the validation (see CapitalAssetInformationValidation). System parameters related to module interfaces should be associated with the depended on module, e.g. CAB parameters related to FP and PURAP collection of CAB data. The data ownership is with CAB, so that is where the parameters belong.
All services / utilities used by more than one optional module will need to reside in org.kuali.kfs.*
per the restrictions on cross-optional module code dependencies. When moving these types of things into KFS core, create an RNE jira to document the work as usual.
In rare cases, we will need to use table(s) as an interface between two optional modules. Those use cases and the specific tables involved were documented on the archived Kuali-Specific Coding Standards page.
OJB Proxying
- In general we try to set proxy=true on reference and collection descriptors for performance reasons.
- In some cases this may require special code, though. And, in other cases it may just not be possible.
- When auto-update=true, there is often a problem with proxy=true
- Another problem case is when there is a chance the reference can be null, and you are displaying a field of the reference (code is ok since you can use ObjectUtils.isNull); in this case set proxy="false"
- If setting proxy=true doesn't work, explicitly set proxy=false rather than remove the setting altogether; this makes it clear that you didn't just forget teh proxy but instead you are purposefully turning it off.
OJB Proxies and testing for nulls
OJB proxies are sadly resistant to null checks. This is because a proxy itself may not be null, but until it actually goes to the database to look for a value, it hasn't replaced itself with null yet. This leads to code examples like this:
if (document.getItem() != null) { // document.item is a proxy - it passes the null check because the proxy isn't null return document.getItem().getQuantity() * 2; // but this blows up because when ojb tries to retrieve the item in getItem() so it can call getAmount(), it finds there is no item in the db and then throws // a very strange NullPointerException }
To help avoid these situations, Rice provides a class org.kuali.rice.krad.util.ObjectUtils (not to be confused with Spring's ObjectUtils or Apache Commons' ObjectUtils) with two methods: isNull() and isNotNull(). These methods force the ORM to check if a proxy is in place and if so, to figure out at that point if the proxy will resolve to null:
if (!ObjectUtils.isNull(document.getItem()) { return document.getItem().getQuantity() * 2; // now, if document.getItem() isn't in the database, this whole block of code will be skipped }
Because of the utility of this method, as well as the fact that it is sometimes difficult to know whether you are dealing with a proxy or not, the KFS product team suggests that you use ObjectUtils.isNull() or ObjectUtils.isNotNull() to perform all null checks.
Static imports / Importing Inner Classes
Static imports (and their cousin - Importing Inner Classes) can be ok in unit test classes, but should be avoided in non-test code. This is to support readability and clarity of code. Consider "CamsConstant.AssetRetirementReasonCode.GIFT" and the following scenarios:
- Unit test that uses this constant repeatedly. The long strings all throughout the file make it tedious to read. A static import alleviates this.
- Long batch class which refers to this constant once. A static import obscures what "GIFT" refers to.
Util classes vs. Services
KFS Currently has several *Util classes typically with static methods. However, Services with public methods on the service interface are preferable to static methods in Util classes, so new code should be placed in public service methods rather than static Util methods.
Using Rice Services
There are a large number of services in the Rice stack. However, not all of them are meant for use by client applications. The vast majority of the services are only used internally by the various modules. The greatest number of services exist in the KEW module. However, only a handful should be used.
When writing Rice-based applications, you will generally only access services from the KIM and KEW modules in normal operation. KNS services all belong to the client and can be accessed freely.
General Rule for KEW Services
I will list some of the services later, but in general, within the KEW module, if the service returns objects whose names end in "Value
", then it is an internal service and should not be used.
Public services in KEW all return "DTO" objects which are capable of being sent over the Kuali Service Bus.
Useful KNS Services
Service | Purpose | Sample Methods |
---|---|---|
Standard KNS Services |
|
|
BusinessObjectService | General purpose service to retrieve and persist business objects. This service eliminates the need for custom DAOs and services for basic CRUD operations. | save( businessObject ) findByPrimaryKey( class, primaryKeyMap ) findBySingleFieldPrimaryKey( class, primaryKeyValue ) findMatching( criteriaMap ) countMatching( criteriaMap ) |
LookupService | Service for performing more complex lookups of business objects. Criteria maps passed to this service can contain special characters in the lookup values which can perform and/or/between/greater than/less than operations. This also can be used to reduce the need for OJB-specific query code. | findCollectionBySearch( class, criteriaMap ) |
MailService | Used to send email messages. | sendMessage( MailMessage ) |
DateTimeService | Use to obtain the current date/time and parse/format dates between String and Date objects. | getCurrentDate() getCurrentSqlDate() |
Data Dictionary Services |
|
|
DataDictionaryService | General purpose service to obtain information about business objects, documents, and their attributes from the data dictionary. These calls are lower level than either of the services below. Only use these calls if you can't get the information from one of the services below. |
|
BusinessObjectDictionaryService | Specialization of the above service to help answer more complex, but common, questions needed about business objects, mainly for implementing the KNS framework. |
|
MaintenanceDocumentDictionaryService | Same as the above, but for maintenance documents. | Class getMaintainableClass(String docTypeName) String getDocumentTypeName(Class businessObjectClass) Class getBusinessObjectClass(String docTypeName) |
Workflow Wrapper Services |
|
|
DocumentService | Primary KNS service for interacting with documents. This service should be used whenever possible instead of accessing KEW services. | Document getNewDocument(String documentTypeName) boolean documentExists(String documentHeaderId) Document getByDocumentHeaderId(String documentHeaderId) Document saveDocument(Document document) routeDocument(...) approveDocument(...) sendAdHocRequests(...) |
Information Services |
|
|
ConfigurationService | Used to pull configuration properties (mostly from configuration.properties). | String getPropertyValueString(String key) boolean getPropertyValueAsBoolean(String key) |
ParameterService | Used to pull parameters from the KRNS_PARM_T table on the Rice server. | parameterExists(...) getParameterValueAsBoolean(...) getParameterValueAsString(...) getParameterValuesAsString(...) |
Public KEW Services
In general, the services below should only be used if there is not a KNS "wrapper" service for the same operation. Often, the KNS services perform additional actions on the document before calling into the workflow engine.
Service | Purpose | Sample Methods |
---|---|---|
WorkflowUtility | main service for obtaining information about a document in routing |
|
WorkflowDocumentActions | allows most actions to be taken on an existing document in routing |
|
DocumentSearchService | Perform document lookups using the same APIs as the document search screen. For programmatic searches, DocumentSearchService has a number of problems - it's incredibly slow, and furthermore, it brings back a limited number of results. Because of this, it would be best to consider if you can find all of the information needed to successfully complete the search by doing a BusinessObjectService#findMatching on records of FinancialSystemDocumentHeader. |
|
Public KIM Services
Service | Purpose | Sample Methods |
---|---|---|
IdentityService | Service to access principal and entity information. | getPrincipalByEmployeeId() getPrincipalByPrincipalName() |
RoleService | Information about Roles | getRole() getRoleByNamespaceCodeAndName() |
PermissionService | Service which checks if a given principal has the permission to execute a certain kind of system event. | isAuthorizedByTemplate() findPermissionsByTemplate() |
PersonService | Full information about entities stored in KIM. IdentityService's getPrincipal*() methods are much faster and typically require all the information necessary to do, say, a permission check - for this reason, those methods are preferred to using PersonService. | getPersonByPrincipalName() |