Kuali Enterprise Workflow (KEW) has a plugin framework that allows you to load code into the core system without requiring changes to the KEW source code or configuration. This framework provides:
A custom class loading space
Hot deploy and reload capabilities
Participation in Workflow's JTA transactions
An application plugin for installation of routing components
Use an application plugin to deploy an application area's routing components into Workflow. These routing components might include:
Rule attributes
Searchable attributes
Post processors
Route modules
If these components require access to a data source, then the application plugin also configures the data source and allows it to participate in Workflow's JTA transactions.
In addition to routing components, the application plugin can also configure a plugin listener and a Resource Loader. The Resource Loader is responsible for loading resources (both Java classes and service implementations) from the plugin and providing them to the core system.
Application plugins are hot-deployable, so a restart of the server is not required when they are added or modified. The core system searches for plugins in a directory configured in the application configuration (see KEW Module Configuration).
You build the plugin as a set of files and directories. You then zip this structure and place it in the appropriate Workflow plugin space. For application plugins, this directory location is defined in the core system configuration.
The name of the zip file (minus the .zip extension) is used as the name of the plugin. The Plugin Loader only looks for files that end in .zip when determining whether to load and hot-deploy a plugin.
In general, application plugins can be named as desired. However, there is one reserved plugin name:
shared - A special plugin that provides a shared classloading space to all plugins (see Plugin Shared Space).
The directory structure of a plugin is similar to that of a web application. It should have this structure:
classes/ lib/ META-INF/ workflow.xml
classes - All java .class files that are used by the plugin should reside in this directory
lib - All .jar library files that are used by the plugin should reside in this directory
META-INF - The workflow.xml configuration file must reside in this directory
Application plugins usually provide a subset of the functionality that an institutional plugin provides, since the institutional plugin can provide core service overrides.
The plugin framework provides two configuration points:
Plugin XML Configuration (described below)
Transaction and DataSource Configuration
The XML configuration is defined in a file called workflow.xml. The format of this file is relatively simple. An example workflow.xml file:
<plug-in> <param name="my.param.1">abc</param> <param name="my.param.2">123</param> <listener> <listener-class>org.kuali.rice.core.ApplicationInitializeListener</listener-class> </listener> <resourceLoader class="my.ResourceLoader"/> </plug-in>
We'll explain each of these elements in more detail below:
The parameter configuration uses XML as the syntax. These parameters are placed into a configuration context for the plugin. The configuration inherits (and can override) values from the parent configurations. The configuration hierarchy is core -> institutional plugin -> application plugins.
A plugin can access its configuration using this code:
org.kuali.rice.Config config = org.kuali.rice.Core.getCurrentContextConfig();
You can define one or more listeners that implement the interface org.kuali.rice.kew.plugin.PluginListener. These can be used to receive plugin lifecycle notifications from KEW.
The interface defines two methods to implement:
Invoked when a plugin starts up
public void pluginInitialized(Plugin plugin);
Invoked when a plugin shuts down
public void pluginDestroyed(Plugin plugin);
It is legal to define more than one plugin listener. Plugin listeners are started in the order in which they appear in the configuration file (and stopped in reverse order).
A plugin can define an instance of org.kuali.rice.resourceloader.ResourceLoader to handle the loading of classes and services. When KEW attempts to load classes or locate services, it searches the institutional plugin, then the core, then any application plugins. It does this by invoking the getObject(..) and getService(...) methods on the plugin's ResourceLoader.
If no ResourceLoader is defined in the plugin configuration, then the default implementation org.kuali.rice.resourceloader.BaseResourceLoader is used. The BaseResourceLoader lets you examine the plugin's classloader for objects when requested (such as post processors, attributes, etc.). This is sufficient for most application plugins.
For more information on configuring service overrides in a plugin, see the Overriding Services with a ResourceLoader section below.
Sometimes it is desirable to be able to point in a plugin to classes or library directories outside of the plugin space. This can be particularly useful in development environments, where the plugin uses many of the same classes as the main application that is integrating with Workflow. In these scenarios, configuring an extra Classpath may mean you don’t need to jar or copy many common class files.
To do this, specify these properties in your plugin's workflow.xml file:
extra.classes.dir - Path to an additional directory of .class files or resources to include in the plugin's classloader
extra.lib.dir - Path to an additional directory of .jar files to include in the plugin's classloader
The classloader then includes these classes and/or lib directories into its classloading space, in the same manner that it includes the standard classes and lib directories. The classloader always looks in the default locations first, and then defers to the extra classpath if it cannot locate the class or resource.
The easiest method to configure Datasources and Transactions is through the Spring Framework. Here is a snippet of Spring XML that shows how to wire up a Spring Transaction Manager inside of a plugin:
<bean id="userTransaction" class="org.kuali.rice.jta.UserTransactionFactoryBean" /> <bean id="jtaTransactionManager" class="org.kuali.rice.jta.TransactionManagerFactoryBean" /> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="userTransaction" ref="userTransaction" /> <property name="transactionManager" ref="jtaTransactionManager" /> <property name="defaultTimeout" value="${transaction.timeout}"/> </bean>
The factory beans in the above XML will locate the javax.trasaction.UserTransaction and java.transaction.TransactionManager, which are configured in the core system. These can then be referenced and injected into other beans (such as the Spring JtaTransactionManager).
Once you configure the transaction manager, you also need to configure any DataSources you require. Here's an example of configuring a DataSource that participates in Atomikos JTA transactions (the default Transaction Manager distributed with Rice Standalone).
<bean id="myDataSource" class="com.atomikos.jdbc.nonxa.NonXADataSourceBean"> <property name="uniqueResourceName" value="myDataSource"/> <property name="driverClassName" value="..."/> <property name="url" value="..."/> ... </bean>
So, the application can access it's datasource by either injecting it into Spring services or by fetching it directly from the Spring context.
You can find more information on configuring Rice DataSources and TransactionManagers in Datasource and JTA Configuration.
If your plugin needs to use OJB, there are a few other configuration steps that you need to take. First, in your Spring file, add the following line to allow Spring to locate OJB and the JTA Transaction Manager:
<bean id="ojbConfigurer" class="org.kuali.rice.ojb.JtaOjbConfigurer"> <property name="transactionManager" ref="jtaTransactionManager" /> </bean>
Next, for OJB to plug into Workflow's JTA transactions, you need to modify some settings in the plugin's OJB.properties file (or the equivalent):
PersistenceBrokerFactoryClass=org.apache.ojb.broker.core.PersistenceBrokerFactorySyncImpl ImplementationClass=org.apache.ojb.odmg.ImplementationJTAImpl OJBTxManagerClass=org.apache.ojb.odmg.JTATxManager ConnectionFactoryClass=org.kuali.rice.ojb.RiceDataSourceConnectionFactory JTATransactionManagerClass=org.kuali.rice.ojb.TransactionManagerFactory
The first three properties listed are part of the standard setup for using JTA with OJB. However, there are custom Rice implementations:
org.kuali.rice.ojb.RiceDataSourceConnectionFactory
org.kuali.rice.ojb.TransactionManagerFactory
org.kuali.rice.ojb.RiceDataSourceConnectionFactory
This OJB ConnectionFactory searches your Spring Context for a bean with the same name as your jcd-alias. Here is what an OJB connection descriptor might look like inside of a Workflow plugin:
<jdbc-connection-descriptor jcd-alias="myDataSource" default-connection="true" platform="Oracle9i" jdbc-level="3.0" eager-release="false" batch-mode="false" useAutoCommit="0" ignoreAutoCommitExceptions="false"> <sequence-manager className="org.apache.ojb.broker.util.sequence.SequenceManagerNextValImpl" /> <object-cache class="org.apache.ojb.broker.cache.ObjectCachePerBrokerImpl"/> </jdbc-connection-descriptor>
Notice that the jcd-alias attribute matches the name of the DataSource Spring bean defined in the example above.
Another important thing to notice in this configuration is that useAutoCommit is set to 0. This tells OJB not to change the auto commit status of the connection because it is being managed by JTA.
Finally, when your plugin needs to use OJB, you need to use this:
org.kuali.rice.ojb.TransactionManagerFactory
This provides OJB with the javax.transaction.TransactionManager that was injected into your JtaOjbConfigurer, as in the example above.
For a service override, you need to define a custom ResourceLoader implementation and configure it in your workflow.xml plugin configuration file. The org.kuali.rice.resourceloader.ResourceLoader interface defines this relative method:
public Object getService(javax.xml.namespace.QName qname);
When KEW is searching for services, it invokes this method on its plugins' ResourceLoader implementations. The service name is a qualified name (as indicated by the use of javax.xml.namespace.QName), but for services being located from the core, service names typically contain only a local part and no namespace.
The easiest way to implement a custom ResourceLoader is to create a class that extends from org.kuali.rice.resourceloader.BaseResourceLoader and just override the getService(QName) method. The BaseResourceLoader provides standard functionality for loading objects from ClassLoaders, among other things.
For example, if you want to override the User Service, you might implement this ResourceLoader:
public class MyResourceLoader extends BaseResourceLoader { public MyResourceLoader() { super(new QName("MyResourceLoader")); } @Override public Object getService(QName serviceName) { if ("enUserOptionsService".equals(serviceName.getLocalPart()) { // return your custom implementation of org.kuali.rice.kew.useroptions.UserOptionsService } else if (...) { ... } else if (...) { ... } return super.getService(serviceName); } }
In the next section, we'll look at some of the services commonly overridden in an institutional plugin
In theory, you can override any service defined in the org/kuali/workflow/resources/KewSpringBeans.xml file in the Institutional Plugin. What follows is a list of the most commonly overridden services:
Table 3.18. Commonly Overridden Services
Service Name | Interface | Description |
---|---|---|
enUserOptionsService | org.kuali.rice.kew.useroptions.UserOptionsService | Provides User lookup and searching services |
IdentityHelperService | org.kuali.rice.kew.identity.service.IdentityHelperService | Interfaces with KIM identity management services |
enEmailService | org.kuali.rice.kew.mail.service.impl.DefaultEmailService | Provides email sending capabilities |
enNotificationService | org.kuali.rice.ken.service.NotificationService | Provides callbacks for notifications within the system |
enEncryptionService | org.kuali.rice.core.service.EncryptionService | Allows for pluggable encryption implementations |
The Workflow core uses the UserService to resolve and search for users. The UserService could be as simple as a static set of users or as complex and integrated as a university-wide user system. Your institution may choose how to implement this, as long as you provide capabilities for the ID types that you intend to use. At the very least, implementations are required for the WorkflowUserId and AuthenticationUserId types (and their corresponding VO beans). All of the UserId types must be unique across the entire set of users.
The WorkflowUserId is typically associated with a unique numerical sequence value and the AuthenticationUserId is typically the username or network ID of the user.
The default UserService implementation provides a persistent user store that allows you to create and edit users through the GUI. It also caches users for improved performance and implements an XML import for mass user import. Institutions usually override the default user service with an implementation that integrates with their own user repository.
The IdentityHelper service helps to interact with the KIM identity management services in the system. IdentityHelpers are identified in one of two ways:
PrincipalId - A numerical identifier for a KIM principal
Group – An object associated with a group of principal users numerical identifier assigned to a Workgroup
Both of these object variables are implemented in KEW in the IdentityHelperServiceImpl file.
The Email service is used to send emails from KEW. You can configure the default implementation when you configure KEW (see KEW Configuration). However, if more custom configuration is needed, then you can override the service in the plugin.
For example, you could override this service if you need to make a secure and authorized SSL connection with an SMTP server because of security policies.
The Notification service is responsible for notifying users when they receive Action Items in their Action List.
The default implementation simply sends an email (using the EmailService) to the user according to the individual user’s preferences. A custom implementation might also notify other (external) systems in addition to sending the email.
The Encryption service is responsible for encrypting document content.
The default implementation uses DES to encrypt the document content. If the encryption.key configuration parameter is set as a Base64 encoded value, then the document content is encrypted using that key. If it is not set, then document content will not be encrypted and will be stored in the database in plain text.
All plugins also load certain classes from a shared space. The shared space contains certain classes that link with certain libraries that might exist in each application or institutional plugin's classloader (such as OJB and Spring). Current classes that Workflow publishes in the shared space are those in the shared module of the Rice project (rice-shared-version.jar). This is important because some of these classes link with libraries like Spring or OJB and since the plugin needs its own copy of these libraries, it needs to ensure that it doesn't retrieve these classes from any classloader but it's own.