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;
      }  
      
    }
7 comments:
 
22. Aug 2008, 21:32 America/New_York | Link

Great work, thanks for sharing. If someone knows a way to use it with SVN instead of CVS that would be great too.

Some comments:

- Your SAX parser is looking for the processing instruction JbpmVersioningInstaller while your documentation use JbpmExtensions

- I had problem using @CreateProcess after deploying processes that way in seam 2.0.3 RC1 .. but I know that the process definitions were nicely deployed. I don't know where the problem lies.

 
03. Sep 2008, 18:31 America/New_York | Link

We are trying to get an application built that would allow us to add new business processes and incorporate both SEAM and JBPM. I am able to do this from the older jbpm-starters-kit-3.1.3 where we are modifying the jbpm console for our needs. We have not been able to do anything like this with the latest jbpm-server-3.2.2 nor have we found a way to add JBPM to JBoss AS 4.2.2.GA. We want to be able to deploy new processes from Eclipse to the JBoss AS to a process repository instead of just as an ear. Other ears then can run processes depending upon the users roles, etc. We are working to build the console that will support this. Your notes imply that you are doing something very similar.

Do you have a url that points to where there are instructions for adding JBPM functionality to the standard JBoss 4.2.2.GA Application Server?

Thanks,

 
11. Sep 2008, 16:22 America/New_York | Link

@Karl:

Thanks for pointing out the JbpmVersioningInstaller error - I renamed this the PI name while I was writing the article, I have fixed the code in the article accordingly.

Regarding SVN - although I don't use it (I do intend to at some point), I believe SVN supports a keyword replacement concept (see SVN keywords). Then you just need to update components xml to include the property versionPattern that can be used to extract the version (see the end of step 2 of the article).

Do you have a stacktrace / log for the error you get using @CreateProcess - I'm pretty busy at the moment but if I get some spare time I'll take a look.

@John

I'm using seam's jBPM support to deploy a business process. The problem I'm addressing here is: by default seam will always redeploy the process definitions - which is undesirable in a production environment.

If I understand correctly, you wish to deploy a process definition using a console and have that available to all application (ears). I would guess, that once you have a JbpmContext, you can deploy and access definitions from any application (since it is just a stored in a database). But this really isn't something I've looked into.

 
02. Oct 2009, 18:30 America/New_York | Link

Tried it with SVN, (changed the pattern).

It mostly works, but somehow it affects (in my my environment at least) the Seam JBPM annotations processing, - @StartTask, @EndTask are totally ignored,

For example in

<s:button action="#{someAction} task="#{sometask}"/>

action is called, but it doesn't start (or continue) the task (someAction is annotated by @StartTask).

Any ideas about what can be wrong?

Thanks,

 
28. Oct 2009, 10:25 America/New_York | Link

Hi there,

@Dmitry

I've figured out the same behaviour, but using Seam 2.2.0GA even the @CreateProcess methods don't do anything. After searching around a bit I figured out that the appropriate Interceptor (BusinessProcessInterceptor) isn't called/used.

Also I changed the class a bit, because it doesn't deploy correctly. Hopefully someone is interested in this issue, because for real-life application a proper (re-)deployment of processes is absolutly necessary.

FYI (the ProcessDescriptor deploy() method):

    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()) ;

        if (newProcessDefinition == null) {
        	log.warn("Process definition #0 - newProcessDefinition is null", getFileProcess().getName());
        	newProcessDefinition = getFileProcess();
        }
         
        // 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 && getDbProcess() != null && !isNewProcess()) {
          // set the old version to negative - only the current version should be positive
          getDbProcess().setVersion(getDbProcess().getVersion() * -1) ;
        }
        return true ;
      } else {
        return false ;
      }
    }

Best Regards, Alexander Müller

 
30. Nov 2009, 16:28 America/New_York | Link
Steve van den Buys | steve.vandenbuys.AT.realdolmen.com

Alexander,

did you figure out why the BusinessProcessInterceptor isn't called? We have already deployed a few Seam applications in a production environment but are now working on our first one that uses jBPM. The solution described here looks much more elegant (and simpler) than actually having to deploy the process definition outside of Seam.

Kind regards,

Steve van den Buys

 
01. Jan 2010, 07:17 America/New_York | Link

Ran into this problem myself over the last couple of days. I needed a way of enabling the timer scheduler and came across this post (might also look into versioning later on). I also ran into the problem where the @CreateTask and @BeginTask annotations were not called. As previously mentioned this is because the BusinessProcessInterceptor is not being installed properly. Before the BusinessProcessInterceptor is installed for a component BusinessProcessInterceptor.isInterceptorEnabled() is called. The method looks like

public boolean isInterceptorEnabled() {
      return Contexts.isApplicationContextActive() && Init.instance().isJbpmInstalled();
}

Init.instance().isJbpmInstalled(); is false.

During the seam bootstrap process Initialization.init() makes a check to see if <bpm:jbpm> exists in your components xml and that installed == true. However because we have replaced that as per the instructions above with a regular component declaration seam thinks jbpm is not installed.

ComponentDescriptor desc = findDescriptor(Jbpm.class);
if (desc != null && desc.isInstalled()) {
   init.setJbpmInstalled(true);
}

Seam then initialises your components and later on when your custom Jbpm class is initialised it sets installed to true:

// work around to let Seam know jbpm is actually installed.
Init.instance().setJbpmInstalled(true);

This however is too late for the BusinessProcessInterceptor.

I have quite a tight deadline at the moment so the quickest solution I could come up with was to specify an empty <bpm:jbpm> block in your components.xml to trick seam into thinking that its installed. This definitely is a hack and I'm sure there is a better solution. If anyone has one please post it. If not I will try to get back to it later and maybe look at extending org.jboss.seam.core.Init or something like that to return true for isJbpmInstalled.

So add something like this to your components.xml

<bpm:jbpm name="notused"></bpm:jbpm>

PS. Is this still the correct way to enable the scheduler? Or is there a built in way now?

 

The Software Factory

www.thesoftfact.com (Seam Software House)