You can find the full source code for this website in the Seam package in the directory /examples/wiki. It is licensed under the LGPL.
The default Seam jBPM deployment system is intended for development only. The reason being: each time the seam environment is started a fresh copy of the process definitions is deployed. This can cause much confusion in a production environment where the definitions should largely be static. Consequently, I found a need to have process definitions deployed only when they change.
I would like to use CVS to determine the version of my process definition. If the CVS version changes - I want it redeployed. Consequently, I have written some code extending the standard Seam Jbpm component to redeploy the process if the CVS version changes.
As a side note: it also allows the jbpm scheduler to be started too.
I hope that you'll find this useful.
<?JbpmExtensions $Revision$?>For example:
<?xml version="1.0" encoding="UTF-8"?>
<?JbpmExtensions $Revision: 1.1 $?>
<process-definition xmlns="urn:jbpm.org:jpdl-3.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:jbpm.org:jpdl-3.2 http://jbpm.org/xsd/jpdl-3.2.xsd"
name="myJbpmProcess">
...
</process-definition> If you don't use CVS: you'll need to include the version manually (or using your source control's particular scheme of keyword replacement) in each file e.g. <?JbpmExtensions version="1"?> and set the versionPattern attribute on the component e.g. <property name="versionPattern">version="([0-9]+)"</property><bpm:jbpm>...</bpm:jbpm>and replace with
<component name="org.jboss.seam.bpm.jbpm" class="uk.co.iblocks.jbpm.JbpmExtensions">
<property name="debugEnabled">false</property> <!-- optional, defaults to true : set to false deploy only if version has changed -->
<property name="schedulerEnabled">true</property><!-- optional, defaults to false: set to true to enable jbpm timers -->
<property name="versionPattern">\$Revision: [0-9]+\.([0-9]+) \$</property><!-- optional, defaults to cvs pattern: Must contain exactly one capture group -->
<property name="processDefinitions">
<value>WEB-INF/jbpm/myJbpmProcess/processdefinition.xml</value>
...
</property>
</component> package uk.co.iblocks.jbpm;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.bpm.Jbpm;
import org.jboss.seam.core.Init;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;
import org.jboss.seam.util.Resources;
import org.jbpm.JbpmContext;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.job.executor.JobExecutor;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* <b>JbpmExtensions.java</b><br>
*
* An extension of the jbpm component to allow conditional deployment of the busines processes.
* Only processes that have been modified since last deployment will be installed.
*
* Each process definition must contain a xml processing instruction that defines the current version.
* By default this is, for example:
* <?JbpmExtensions $Revision: 1.3 $?>
*
* The component must be setup in components.xml as follows:
* <component name="org.jboss.seam.bpm.jbpm" class="uk.co.iblocks.jbpm.JbpmExtensions">
* <property name="processDefinitions">
* <value>processDefinion.xml</value>
* </property>
* </component>
*
* @author <a href="mailto:peter.brewer@iblocks.co.uk">Peter Brewer</a>
*/
@BypassInterceptors
@Startup
public class JbpmExtensions extends Jbpm {
private final class ProcessDescriptor {
private Integer fileVersion ;
private ProcessDefinition fileProcess ;
private ProcessDefinition dbProcess ;
public ProcessDescriptor(JbpmContext jbpmContext, String definitionResource) throws SAXException, IOException {
setFileVersion( versionHandler.parse(definitionResource) ) ;
setFileProcess( ProcessDefinition.parseXmlResource(definitionResource) ) ;
setDbProcess( jbpmContext.getGraphSession().findLatestProcessDefinition(getFileProcess().getName())) ;
}
public Integer getFileVersion() {
return fileVersion;
}
public void setFileVersion(Integer fileVersion) {
this.fileVersion = fileVersion;
}
public ProcessDefinition getFileProcess() {
return fileProcess;
}
public void setFileProcess(ProcessDefinition fileProcess) {
this.fileProcess = fileProcess;
}
public ProcessDefinition getDbProcess() {
return dbProcess;
}
public void setDbProcess(ProcessDefinition dbProcess) {
this.dbProcess = dbProcess;
}
public boolean isNewProcess() {
return getDbProcess() == null || (getFileVersion() != null && getFileVersion() > getDbProcess().getVersion()) ;
}
public boolean deploy(JbpmContext jbpmContext) {
return deploy(jbpmContext, false) ;
}
public boolean deploy(JbpmContext jbpmContext, boolean forceDeployment) {
if (forceDeployment || isNewProcess()) {
log.info("Deploying process #0 - replacing db version #1 with file version #2", getFileProcess().getName(), getDbProcess() != null ? String.valueOf(getDbProcess().getVersion()) : "<undeployed>", getFileVersion()) ;
jbpmContext.deployProcessDefinition(getFileProcess());
ProcessDefinition newProcessDefinition = jbpmContext.getGraphSession().findLatestProcessDefinition(getFileProcess().getName()) ;
// Note this overrides the jbpm automated versioning system to keep the file version and db version the same.
if (getFileVersion() != null) {
newProcessDefinition.setVersion( getFileVersion() != null ? getFileVersion() : getDbProcess().getVersion()) ;
}
if (forceDeployment && !isNewProcess()) {
// set the old version to negative - only the current version should be positive
getDbProcess().setVersion(getDbProcess().getVersion() * -1) ;
}
return true ;
} else {
return false ;
}
}
}
private static final class VersionHandler extends DefaultHandler {
private int version = -1 ;
private boolean versionPIProcessed = false ;
private Pattern versionPattern = null ;
private SAXParser parser = null ;
public VersionHandler(String versionPattern) {
this.versionPattern = Pattern.compile(versionPattern) ;
SAXParserFactory sf = SAXParserFactory.newInstance() ;
sf.setValidating(false);
sf.setNamespaceAware(false);
try {
parser = sf.newSAXParser() ;
} catch (Exception ex) {
log.fatal("Cannot create xml parser.", ex) ;
throw new IllegalStateException("Cannot create xml parser.") ;
}
}
public Integer parse(String definitionResource) throws SAXException, IOException {
reset() ;
InputStream xmlStream = null ;
try {
URL processUrl = Resources.getResource(definitionResource, null) ;
xmlStream = processUrl.openConnection().getInputStream() ;
parser.parse(new InputSource(xmlStream), this);
return getVersion() ;
} finally {
if (xmlStream != null) {
try {
xmlStream.close() ;
} catch (IOException e) {
log.debug("Cannot close xml stream for #0", e, definitionResource) ;
}
}
}
}
public void processingInstruction(String target, String data) throws SAXException {
if (PI_TARGET.equals(target)) {
Matcher m = versionPattern.matcher(data) ;
if (m.matches() && m.groupCount() == 1) {
this.version = Integer.valueOf( m.group(1) );
} else {
log.warn("Found processing instruction but data does not match the pattern (or the pattern doesn't have exactly one capture group). Expected patten: #0", versionPattern.toString()) ;
}
versionPIProcessed = true ;
}
}
public boolean isVersionPresent() {
return versionPIProcessed ;
}
/** Currently returns the minor version number (in our cvs we just use 1.x, so its fine)
* However, if we switched to incrementing the major number, then we'd have trouble.
*/
public Integer getVersion() {
if (isVersionPresent()) {
return this.version ;
} else {
return null ;
}
}
public void reset() {
this.version = -1 ;
this.versionPIProcessed = false ;
}
}
private static final Log log = Logging.getLog(JbpmExtensions.class);
private static final String PI_TARGET = "JbpmExtensions" ;
private boolean debugEnabled = true ;
private boolean schedulerEnabled = false ;
private String versionPattern = "\\$Revision: [0-9]+\\" +
".([0-9]+) \\$" ;
private VersionHandler versionHandler ;
private boolean workflowDependenciesEnabled = false ;
private Map<String, ProcessDescriptor> processDescriptors ;
/**
* Returns the regular expression pattern used for determining the version specified in the
* xml processing instruction.
* @return
*/
public String getVersionPattern() {
return versionPattern;
}
public void setVersionPattern(String versionPattern) {
this.versionPattern = versionPattern;
}
/**
* Returns whether debug (non-production) is switch on.
* @return
*/
public boolean isDebugEnabled() {
return debugEnabled;
}
public void setDebugEnabled(boolean debug) {
this.debugEnabled = debug;
}
/**
* Returns where the jbpm scheduler is enabled.
* @return
*/
public boolean isSchedulerEnabled() {
return schedulerEnabled;
}
public void setSchedulerEnabled(boolean schedulerEnabled) {
this.schedulerEnabled = schedulerEnabled;
}
/**
* Prevents the default jbpm component from installing all processes.
*/
@Override
protected boolean isProcessDeploymentEnabled() {
return false ;
}
/**
* Overrides the default component to use conditional deployment.
*/
@Override
public void startup() throws Exception {
log.info("Using jBPM extensions. debug #0, dependencyHandling #1, scheduler #2", isDebugEnabled() ? "enabled" : "disabled", isWorkflowDependenciesEnabled() ? "enabled" : "disabled", isSchedulerEnabled() ? "enabled" : "disabled") ;
super.startup();
versionHandler = new VersionHandler( getVersionPattern() ) ;
processDescriptors = new HashMap<String, ProcessDescriptor>() ;
// work around to let Seam know jbpm is actually installed.
Init.instance().setJbpmInstalled(true) ;
// let the user know if nothing was deployed.
if ( !installProcessDefinitions() ) {
log.info("No process definitions have changed, so nothing was deployed.") ;
}
if (isSchedulerEnabled()) {
log.info("Starting the jBPM scheduler");
startScheduler() ;
if (isRunning()) {
log.info("jBPM scheduler has started.");
} else {
log.error("jBPM scheduler was not started.") ;
}
}
}
/**
* Go through each process definition and conditionally deploy it.
*
* @return true if at least one process definition was deploy, false otherwise.
*/
private boolean installProcessDefinitions() {
boolean installed = false ;
JbpmContext jbpmContext = getJbpmConfiguration().createJbpmContext();
try {
if (getProcessDefinitions() != null) {
for (String definitionResource : getProcessDefinitions()) {
if (isDebugEnabled()) {
// If debug is enabled, process definitions are always deployed.
// Note: in order to maintain consistent versioning,
// jbpm tables ought to be cleared out when switching from debug to production
jbpmContext.deployProcessDefinition( ProcessDefinition.parseXmlResource(definitionResource) ) ;
installed = true ;
log.info("Debug mode enabled - deploying process definition: #0", definitionResource);
} else {
ProcessDescriptor processDescriptor = new ProcessDescriptor(jbpmContext, definitionResource) ;
boolean deployed = processDescriptor.deploy(jbpmContext) ;
if (!deployed && workflowDependenciesEnabled) {
// save for later in case we need to redeploy everything (i.e. assume dependencies)
processDescriptors.put(processDescriptor.getFileProcess().getName(), processDescriptor) ;
}
installed = installed || deployed ;
}
}
// at least one process has deployed, so deploy the others
if (!isDebugEnabled() && installed && !processDescriptors.isEmpty()) {
for (ProcessDescriptor pd : processDescriptors.values()) {
// force redeployment
pd.deploy(jbpmContext, true) ;
}
}
}
return installed ;
} catch (Exception e) {
jbpmContext.getSession().getTransaction().rollback() ;
throw new RuntimeException("Could not deploy a process definition.", e);
} finally {
jbpmContext.close();
}
}
/**
* Returns the jbpm job executor.
*/
public JobExecutor getJobExecutor() {
return getJbpmConfiguration().getJobExecutor() ;
}
/**
* Starts the jbpm scheduler
*/
private void startScheduler() {
JobExecutor jobExecutor = getJobExecutor() ;
if (jobExecutor != null) {
jobExecutor.start() ;
}
}
/**
* Stops the jbpm scheduler.
*/
private void stopScheduler() {
JobExecutor jobExecutor = getJobExecutor() ;
if (jobExecutor != null) {
try {
jobExecutor.stopAndJoin() ;
} catch (InterruptedException e) {
log.warn( "Could not wait for job executor.", e ) ;
}
}
}
/**
* Returns true if the jbpm scheduler is running.
* @return
*/
private boolean isRunning() {
return getJobExecutor() != null && getJobExecutor().isStarted() ;
}
/**
* Overridden to stop the jbpm scheduler if its running.
*/
@Override
public void shutdown() {
if (isRunning()) {
log.info("Stopping the jBPM scheduler.");
stopScheduler() ;
} else if ( isSchedulerEnabled() ){
log.debug("jBPM Scheduler can't be stopped because it was not running.");
}
super.shutdown() ;
}
public boolean isWorkflowDependenciesEnabled() {
return workflowDependenciesEnabled;
}
/**
* Set to true to try and work around early binding of jbpm (experimental). Set to false if
* no dependencies occur in the workflow or if late binding attribute is used.
* @param workflowDependenciesEnabled
*/
public void setWorkflowDependenciesEnabled(boolean workflowDependenciesEnabled) {
this.workflowDependenciesEnabled = workflowDependenciesEnabled;
}
}