Auf dem Laufenden bleiben?
Erhalten Sie immer die neuesten Infos!

This is a short Tutorial on building Java Swing user interfaces for JBoss jBPM using the tk4jbpm.

1 Introduction
2 Remote-Communication
3 The example application
4 GUI components and screenshots

5 tk4jbpm configuration
6 Installation

6.1 Requirements for the example application
6.2 Installation steps
6.3 possible problems

Introduction

This short tutorial gives a short overview on implementing a Java Swing GUI for process oriented applications with JBoss jBPM using the tk4jbpm. A detailed description on that task will be given in a article in the german Java Magazin (see publications), so I will concentrate in this short tutorial only on the screens and the code. Especially the part about what is different in user interfaces for BPM applications ist left out here. Please read the article for a deeper understanding (sorry that it is in german).

For the introduced example application the tk4jbpm is used, which you can download from sourceforge (see download on the left). The sources for the example you can download as one zip (with all necessary libraries included) from here.

Remote-Communication

One big problem with Rich Clients, you don’t have with Webinterfaces, is the remote communication. Because your client resides in a different virtual machine than jbpm on the server. This has the impact, that we need to think about what data we have to transfer from the server to the client. Data Transfer Objects were used for this task in history but have some drawbacks (ask Google for details). So the solution introduced in jBPM 3.2 is different and is based on the Command Pattern. So today most of the important jBPM features is available as commands (I also committed the code in the AdminServiceBean “ of older version of the tk4jbpm to the jbpm core as commands). To support remote communication it is the easy to implement a CommandService as EJB 3 Session Bean. See the APIDOC: com.camunda.toolkit.jbpm.service.CommandServiceBean for details:

@Stateless
@Remote( {CommandService.class})
@Local( {CommandService.class})
@SecurityDomain("camunda-tk-jbpm")
public class CommandServiceBean implements CommandService {

   @Resource(mappedName = "java:/jbpm/JbpmConfiguration",
             name = "jbpm/JbpmConfiguration")
   private org.jbpm.JbpmConfiguration jbpmConfiguration;

   @TransactionAttribute(value = TransactionAttributeType.REQUIRED)
   public Object execute(Command command) {
       JbpmContext jbpmContext = jbpmConfiguration.createJbpmContext();
       try {
           return command.execute(jbpmContext);
       }
       catch (Exception e) {
           if (RuntimeException.class.isAssignableFrom(e.getClass()))
               throw (RuntimeException)e;
           else
               throw new JbpmException("couldn't execute " + command.getClass().
                                    getSimpleName() + ": " + e.getMessage(), e);
       }
       finally {
       	try {
       		jbpmContext.close();
       	}
       	catch (Throwable ex) {
       		log.error("could not close jbpm context after command "
       		          + command.getClass().getName() , ex);
       	}
       }
   }
}

The example application

The example application is a very simple order management system. Therefor we have a EJB 3 Entity which models the Order and a EJB 3 SessionBean as facade to it. You can see this very easy business logic here:

The process contains, beside Start and End-State, only of a State, where we wait for the payment and a task-node, where somebody has to ship the order. Suprisingly, the resulting process diagram ist very easy:

The jPDL source is straighforward then. The own TaskController ist just included to demonstrate how a alternative panel mapping could be implemented, which is described in the JavaMagazin article.

<process-definition
  xmlns="urn:jbpm.org:jpdl-3.2"  name="SimpleOrder">

   <swimlane name="Lager">
      <assignment pooled-actors="Lager" />
   </swimlane>

   <start-state name="Auftragseingang">
      <event type="node-leave">
         <action name="create order in business logic"
           class="com.camunda.jmgui.actions.CreateOrderAction" />
      </event>
      <transition name="Auftrag anlegen" to="Warten auf Zahlung" />
   </start-state>
   <state name="Warten auf Zahlung">
      <transition name="Zahlung OK" to="Auftrag versenden" />
      <transition name="Zahlung nicht korrekt" to="Warten auf Zahlung" />
   </state>
   <task-node name="Auftrag versenden">
      <task name="Auftrag versenden" swimlane="Lager">
         <controller class="com.camunda.jmgui.web.NavigationTaskController">
         	<viewName>shipOrder</viewName>
         </controller>
      </task>
      <event type="node-leave">
         <action name="set order shipped status in business logic"
                 class="com.camunda.jmgui.actions.OrderShippedAction" />
      </event>
      <transition name="erledigt" to="Auftrag versendet" />
   </task-node>
   <end-state name="Auftrag versendet" />
</process-definition>

For the user interface we need diferent components now:

  • a panel to start a order process
  • a search to find orders if the payment is received
  • a panel to check if payment is correct and give the result to jBPM
  • a task list to indicate open tasks, which is „Auftrag versenden“ („ship order“) in our case
  • a panel where we see the information about the content of the order, so the eomplyeee can ship it

Not really needed, but very handy, is a admin client, where we can really see what processes are going on, in which state and so on.

GUI components and screenshots

Now we want to make a short virtual walk through the application by some screenshots of the above described components. By the way, the default username is test with passwort test.

1. menu of application

the manue of the application is build by scruffy with the following source code

private void buildMenu() {
 WindowManager wm = ScruffyManager.getMainWindowManager();
 ScruffyManager.getInstance().addMenu("Datei:BpmAdmin",
    new ShowComponentAction(JbpmAdminGuiComponent.class, wm));
 ScruffyManager.getInstance().addMenu("Datei:Worklist",
    new ShowWorklistComponent(wm, new String[] { "Lager" }));
 ScruffyManager.getInstance().addMenu("Datei:OpenToken",
    new OpenProcessWithTokenIdAction(wm));
 ScruffyManager.getInstance().addMenuSeperator("Datei:---");
 ScruffyManager.getInstance().addMenu("Datei:StartOrder",
    new StartSelectedProcessWithFormAction(wm, "SimpleOrder"));
 ScruffyManager.getInstance().addMenu("Datei:AuftragSuche",
    new ShowSearchComponentAction(OrderSearch.class, wm));
 ScruffyManager.getInstance().addMenuSeperator("Datei:---");
 ScruffyManager.getInstance().addMenu("Datei:ExceptionLog",
    new OpenExceptionLogAction(wm));
 ScruffyManager.getInstance().addMenu("Datei:End",
    new TerminateScruffyAction());
}

and looks like:

I think a long explenation on how to construct the menu is not needed, just play around with it.

2. start a order process

To start a new order process we just type in the name of the customer. To keep it very simple thats all. We have to take care, that the customer name ist also set in the process variables while changing it in the gui, which is done via JGoodies DataBinding.

The source code is also not very complicated. As a LayoutManager I used JGoodies FormLayout, but every LayoutManager can do the job. It is just, that I am quite familiar and happy with the JGoodies framework.

public class StartOrderPanel extends AbstractProcessDetailBasePanel {

 private SimpleOrderProcessModel model;

 private ValueModel customerNameModel = new ValueHolder(""); 

 public StartOrderPanel(ProcessModel model) {
  super(model);
  this.model = (SimpleOrderProcessModel)model;
 }

 @Override
 public void initialize() {
  model.addEmptyNewOrder();
  customerNameModel.addValueChangeListener(new PropertyChangeListener() {
   public void propertyChange(PropertyChangeEvent arg0) {
    model.getNewOrder().setCustomerName( (String)customerNameModel.getValue() );
   }
  });
 }

 @Override
 public JComponent build() {
  FormLayout layout = new FormLayout("pref, 2dlu, 80dlu:grow", "pref");
  JPanel panel = new JPanel();
  DefaultFormBuilder builder = new DefaultFormBuilder(layout, panel);
  builder.setBorder(Borders.createEmptyBorder("4dlu, 4dlu, 4dlu, 4dlu"));
  CellConstraints cc = new CellConstraints();             

  builder.addLabel("Kunde", cc.xy(1, 1));
  builder.add(BasicComponentFactory.createTextField(customerNameModel), cc.xy(3, 1)); 

  return panel;
 }
}

The following picture shows the resulting panel. Nothing really spectacular, but working. For bigger applications the interessting point is how to implement the Data-Binding without writing to much code by hand. We have good experiences with code generation and MDA approaches in that field.

3. search a order

Searching a order can be implemented by using the search capabilities of Scruffy. We need 3 components for that:

  • a search class, extending from JBopSearch, which is just a JavaBean and holds the search parameters
  • a search panel, which can bind on this Search and so offers the possibility to change the search parameters via the gui
  • a table model, which can show the results of the search

Also we need a business service, which implements the JBopSearchService interface and is capable to execute the search (and fill the Search class with the results). There are some more details to get the complete idea, but we will skip that for now and just look into the required code. To gather a deeper understanding, I would recommend to play around with the example and maybe have a look in the soruces of scruffy (part of the camunda commons).

OrderSearch

public class OrderSearch extends JBopSearch {

 private String searchString;

 public OrderSearch() {
  super(Order.class);
 }

 @Override
 public String getSearchIdentificationString() {
  return this.getClass().getName() + "[" + searchString + "]";
 }

 @Override
 public void setValue(BoundBean bean) {
  OrderSearch s = ((OrderSearch) bean);
  setSearchString(s.searchString );

  setResult( s.getResult() );
  setResultCount( s.getResultCount() );
 }

 public String getSearchString() {
  return searchString;
 }

 public void setSearchString(String searchString) {
  String oldValue = this.searchString;
  this.searchString = searchString;
  firePropertyChange(PROPERTYNAME_searchString, oldValue, searchString);
 }

 public static final transient String PROPERTYNAME_searchString = "searchString";
}

OrderSearchTableModel

public class OrderSearchTableModel extends ScruffySearchTableModel {

 public OrderSearchTableModel() {
  this (new OrderSearch());
 }

 public OrderSearchTableModel(JBopSearch search) {
  this(search, 50);
 }

 public OrderSearchTableModel(JBopSearch search, int fetchedRows) {
  super(Factory.getOrderService(), // Search Service to use
    search, // search to use
    fetchedRows, // fetch count
    new String[] {"Auftragsnummer", "Name", "Prozess-ID"}, // columns
    new String[] {null, null, "processId"}); // column keys
 }

 @Override
 public Object getColumnValue(Object o, int columnIndex) {
  switch (columnIndex) {
  case 0:
   return ((Order)o).getId();
  case 1:
   return ((Order)o).getCustomerName();
  case 2:
   return ((Order)o).getProcessId();
  }
  return null;
 }
}

OrderSearchPanel

public class OrderSearchPanel extends SearchInputGuiComponent {

 private OrderSearch search;

 private OrderSearchTableModel model;

 public void initializeWithBean(BoundBean bean) {
  if (!OrderSearch.class.isAssignableFrom(bean.getClass())) {
   throw new RuntimeException("OrderSearchPanel can not be initialized with "
                              + bean.getClass().getName());
  }
  this.search = (OrderSearch) bean;
 }

 public String getBusinessClassName() {
  return "OrderSearch";
 }

 public String getBusinessModuleName() {
  return "order";
 }

 public JBopSearch getBean() {
  return search;
 }

 public OrderSearch getSearch() {
  return search;
 }

 public String getComponentDescription() {
  return "Auftrassuche";
 }

 @Override
 protected void setTableModel(ScruffySearchTableModel model) {
  this.model = (OrderSearchTableModel) model;
 }

 public JPanel build() {
  FormLayout layout = new FormLayout(//
    "pref, 6dlu, 100dlu:grow", //
    "pref");
  DefaultFormBuilder builder = new DefaultFormBuilder(layout, new JPanel());
  builder.setBorder(com.jgoodies.forms.factories.Borders.
      createEmptyBorder("4dlu, 4dlu, 4dlu, 4dlu"));
  CellConstraints cc = new CellConstraints();

  PresentationModel model = new PresentationModel( this.search );

  int row = 1;
  builder.addLabel("Name", cc.xywh(1, 1, row, 1));
  builder.add(com.jgoodies.binding.adapter.BasicComponentFactory.createTextField(
                model.getModel(search.PROPERTYNAME_searchString)),
                cc.xywh(3, row, 1, 1));

  return builder.getPanel();
 }
}

and the executeSearch method in our OrderServiceBean:

public JBopSearch executeSearch (JBopSearch search, long startRange, long endRange)
                                throws BusinessException {
 String s = ((OrderSearch)search).getSearchString();
 Query q = entityManager.createQuery(
   "select o from Order o where customerName like :name")
   .setParameter("name", "%" + s + "%");

 // ignore limits for the moment
 //     q.setFirstResult((int)startRange);
 //     q.setMaxResults((int)endRange);

 search.setResult(q.getResultList().toArray());
 search.setResultCount(q.getResultList().size());

 return search;
}

With that components Scruffy does all the rest for you: Layouting the search panel, do search execution in background while showing some progress bar, prefetching only a defined number of rows and lazy load more if you scroll down the table, …

To tie all pices together we have do define a search in the Scruffy repository while setting up our application:

ScruffyManager.getRepository().addSearch(OrderSearch.class, // search class
  OrderSearchPanel.class, // form class
  OrderSearchTableModel.class); // table model

And now you can see how the resulting Search panel looks like:

You can see the link on the process id in the results table. If you look back in the TableModel you can see that we defined a column key named processId there, this is used to magically create that link on the fly.

4. check payment

a click on that link with the token id of the process associated to the orders leads the tk4jbpm to open a panel for that process. If there is aopen task, it is opened, otherwise the panel configured for the state of the token will be shown. In our example application this is the panel configured for the State „Warten auf Zahlung“:

The implementation of this panel is pretty easy and nearly the same like the StartOrderPanel, so we skip the soruce code here.

5. task list

The tasklist is a ready to use component which just consists of a table with all open tasks for the user. A click on one row opens the configured task panel for this task.

6. ship order

The panel to ship the order is again very easy, no need to explain anything:

additional components

There are two additional components used in the example application:

  • the tk4jbpm admin-client, which can show all informations about jbpm, like deployed processes, process instances (running or ended) with process variables, logs and more. In older versions of the toolkit you could also change or add process variables with the admin-client, I work to reintroduce that feature in the current version to
  • the scruffy exception log. If there are any Exceptions in Scruffy-Actions, they are not only shown via some Dialog but also added in this Log. So you can later see the Exception with stacktrace there, so users can easily add them to some bug report or send you via mail.

You can get a impression on these components in the next screenshots:

tk4jbpm configuration

The configuration of tk4jbpm is not very long, since we only have three panels (start panel included) to configure.

<?xml version="1.0" encoding="UTF-8"?>
<tk4jbpm>
 <process name="SimpleOrder" model="com.camunda.jmgui.swing.SimpleOrderProcessModel">
   <start-state name="Auftragseingang"
      start-class="com.camunda.jmgui.swing.StartOrderPanel" />
   <state name="Warten auf Zahlung"
      start-class="com.camunda.jmgui.swing.CheckPaymentPanel">
     <transition name="Zahlung OK" confirm="true" />
   </state>
   <state name="Auftrag versenden"
      start-class="com.camunda.jmgui.swing.ShipmentPanel" />
 </process>
</tk4jbpm>

I hope everythin is intuitive. Unfortunately a more sophisticated documentation of the complete fearure set and all configuration options is still a open task at the moment.

The start class get all service references and configures tk4jbpm and scruffy. After this is done, the Mainframe can be started. Thats all.

public class SwingMain {

 private static InitialContext ctx;

 public static void main(String[] args) throws Exception {
  new SwingMain().initApplication();
 }

 public void initApplication() throws Exception {
  initInitialContext();
  setLookAndFeel();
  initTk4jbpm();
  initBusinessLogicFactory();
  configureScruffy();
  buildMenu();
  ScruffyManager.getMainWindowManager().createAndShow();
 }

 private void initInitialContext() throws IOException, NamingException {
  InputStream in = this.getClass().getResourceAsStream("/jndi.properties");
  Properties p = new Properties();
  p.load(in);
  ctx = new InitialContext(p);
 }

 private void setLookAndFeel() {
  try {
   UIManager.setLookAndFeel(new PlasticXPLookAndFeel());
  }
  catch (Exception ex) { // ignore
  }
 }

 private void initBusinessLogicFactory() throws Exception {
  OrderService orderService = (OrderService) ctx.lookup("/jmgui/OrderService/remote");
  Factory.initOrderService(orderService);
 }

 private void initTk4jbpm() throws Exception {
  CommandService commandService = (CommandService)
     ctx.lookup("/jmgui/CommandServiceBean/remote");
  Tk4jbpmConfiguration.initTk4jbpm(commandService, "/tk4jbpm.xml", false);
  Tk4jbpmConfiguration.setUseScruffy(true);
 }

 private void configureScruffy() {
  ScruffyManager.init();
  ScruffyManager.initLoginService(new LoginService());

  JaasJbossConfiguration.activateConfiguration();
  ScruffyManager.login();

  ServiceBroker.registerService("tk4jbpmDelegate", new JbpmScruffyDelegate());

  ScruffyManager.getInstance().setTitle("camunda jBPM GUI example");
  ScruffyManager.getInstance().setRepository(new DefaultScruffyRepository());

  new JbpmScruffyConfiguration().addToRepository(ScruffyManager.getRepository());
  new ScruffyConfig().addToRepository(ScruffyManager.getRepository());
 }

 private void buildMenu() {
  WindowManager wm = ScruffyManager.getMainWindowManager();
  ScruffyManager.getInstance().addMenu("Datei:BpmAdmin",
    new ShowComponentAction(JbpmAdminGuiComponent.class, wm));
  ScruffyManager.getInstance().addMenu("Datei:Worklist",
    new ShowWorklistComponent(wm, new String[] { "Lager" }));
  ScruffyManager.getInstance().addMenu("Datei:OpenToken",
    new OpenProcessWithTokenIdAction(wm));
  ScruffyManager.getInstance().addMenuSeperator("Datei:---");
  ScruffyManager.getInstance().addMenu("Datei:StartOrder",
    new StartSelectedProcessWithFormAction(wm, "SimpleOrder"));
  ScruffyManager.getInstance().addMenu("Datei:AuftragSuche",
    new ShowSearchComponentAction(OrderSearch.class, wm));
  ScruffyManager.getInstance().addMenuSeperator("Datei:---");
  ScruffyManager.getInstance().addMenu("Datei:ExceptionLog",
    new OpenExceptionLogAction(wm));
  ScruffyManager.getInstance().addMenu("Datei:End",
    new TerminateScruffyAction());
 }
}

Installation

Requirements for the example application

  • download example application here.
  • JBoss application server. The client libraries delivered within th ezip file are for version 4.0.4 (EJB3 profile). But it should not be a problem to use another version, if you update the jboss client libs.
  • Maybe a IDE. Eclipse with jBPM plugin is a good choice, if you want to see the jbpm plugin in action. The JBoss IDE would be a ready to use package.
  • Java 5 or Java 6
  • Ant (no special requirements on version)
  • a database. If you don’t want to change database drivers or build properties, use mySQL 5. But all other databases are possible, just change the hibernate / EJB 3 options.

Installation steps

  1. modify the build.properties in src/config according to you local environment
  2. create the database (called jmgui in the default preferences)
  3. change ejb3.deployer in JBoss to use cglib (instead of javassit, because you can have serious Permgenspace-Exceptions with javassist)
    • change the last line of ${jboss.deploy.dir}/ejb3.deployer/META-INF/persistence.properties to hibernate.bytecode.provider=cglib
  4. run ‚ant all to build the whole application and deploy everything to JBoss. The deployment includes:
    • copy the required database driver to the JBoss lib directory
    • the ear
    • the datasource
    • sample user & group files for JAAS (because the tk4jbpm SessionBean is configured to use JAAS). As default, user ist test with password test

possible problems