Help

Built with Seam

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.

Requirements

  • Seam 2.0.1.GA
  • jBPM 3.2.2
  • optionally CVS for source control
  • If using <process-state> tags in your processes, its probably best to add the binding="late" attribute see jbpm docs.

Installation

  1. Clear out any existing definitions in the database (if this is not possible make sure that the db version is synched with the cvs version).
  2. Update the start of each process definition xml file to include the following, and then check it in to CVS:
    <?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>
  3. Update the components.xml to override the standard jbpm component, remove the following:
    <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>
  4. Include the following code in your Seam project (or download it: JbpmExtensions.java):
    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;
      }  
      
    }