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