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; } }