2014-02-13

How to create custom form property types in Activiti

Activiti BPM engine provides support for creating forms for user tasks. Out-of-the box the following form property types are supported: string, long, enum, date, boolean. In this post I would like to show you, how you can add custom form property type.

The simplest example
For simple custom form types we only need to do two things: create a class which extends AbstractFormType and register it in activiti.cfg.xml:
public class IntegerFormType extends AbstractFormType {

  @Override
  public String getName() {
    return "integer";
  }

  @Override
  public Object convertFormValueToModelValue(String propertyValue) {
    return Integer.valueOf(propertyValue);
  }

  @Override
  public String convertModelValueToFormValue(Object modelValue) {
    return modelValue != null ? modelValue.toString() : null;
  }
}
<bean id="processEngineConfiguration" 
      class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
  ...
  <property name="customFormTypes">
    <list>
      <bean class="com.github.mateuszwenus.activiti_custom_form_types.IntegerFormType" />
    </list>
  </property>
</bean>
Now we can use it in our process definition:
<userTask id="usertask1" name="User Task">
  <extensionElements>
    <activiti:formProperty id="number" name="Number" type="integer"></activiti:formProperty>
  </extensionElements>
</userTask>
And then load it using Activiti's FormService:
Task task = ...
TaskFormData taskFormData = formService.getTaskFormData(task.getId());
FormType formType = taskFormData.getFormProperties().get(0).getType();
assertThat(formType, instanceOf(IntegerFormType.class));
Custom form property types with parameters
Creating custom integer form property type is not really useful, so let's consider more complex example. Suppose we need a form property type, which would be rendered as a select list with integer values from specified range [from, to]. This is how we would like to use it in process definition file:
<userTask id="usertask1" name="User Task">
  <extensionElements>
    <activiti:formProperty id="rating" name="Select your rating" type="selectFromRange">
      <activiti:value id="from" name="0" />
      <activiti:value id="to" name="10" />
    </activiti:formProperty>
  </extensionElements>
</userTask>
By default <activiti:value> elements are used to configure enum form property types (they specify list of allowed values). We'll use them to pass parameters to our custom form type. This might look strange at first, but has many advantages:
  • we can pass any configuration parameters (everything that fits key-value model)
  • we'll be able to use Activiti API to load our config parameters
  • process XML file is still valid - we didn't add anything new

So let's start implementation. Just like with integer form type, first we need to create an appropriate class:
public class SelectFromRangeFormType extends AbstractFormType {

  public static final String NAME = "selectFromRange";

  private final int from;
  private final int to;

  public SelectFromRangeFormType(int from, int to) {
    this.from = from;
    this.to = to;
  }

  public int getFrom() {
    return from;
  }

  public int getTo() {
    return to;
  }

  @Override
  public String getName() {
    return NAME;
  }

  @Override
  public Object convertFormValueToModelValue(String propertyValue) {
    return Integer.valueOf(propertyValue);
  }

  @Override
  public String convertModelValueToFormValue(Object modelValue) {
    return modelValue != null ? modelValue.toString() : null;
  }
}
This class doesn't have no-arg constructor, so it cannot be simply added to customFormTypes in activiti.cfg.xml - we need another solution. A quick search on Activiti sources reveals that subclasses of AbstractFormType are created by class FormTypes, which is used in DefaultFormHandler to process user task's form configuration. Fortunately DefaultFormHandler gets FormTypes instance from process engine configuration, so we can swap the default for our own implementation:
public class CustomFormTypes extends FormTypes {

  @Override
  public AbstractFormType parseFormPropertyType(FormProperty formProperty) {
    if (SelectFromRangeFormType.NAME.equals(formProperty.getType())) {
      return parseSelectWithRangeFormPropertyType(formProperty);
    } else {
      return super.parseFormPropertyType(formProperty);
    }
  }

  private AbstractFormType parseSelectWithRangeFormPropertyType(FormProperty formProperty) {
    Map map = new HashMap();
    for (FormValue fv : formProperty.getFormValues()) {
      map.put(fv.getId(), fv.getName());
    }
    int from = Integer.parseInt(map.get("from"));
    int to = Integer.parseInt(map.get("to"));
    return new SelectFromRangeFormType(from, to);
  }
}
In activiti.cfg.xml:
<bean id="processEngineConfiguration" 
      class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
  ...
  <property name="formTypes">
    <bean class="com.github.mateuszwenus.activiti_custom_form_types.CustomFormTypes" />
  </property>
  <property name="customFormTypes">
    <list>
      <bean class="org.activiti.engine.impl.form.StringFormType" />
      <bean class="org.activiti.engine.impl.form.LongFormType" />
      <bean class="org.activiti.engine.impl.form.DateFormType">
        <constructor-arg value="dd/MM/yyyy" />
      </bean>
      <bean class="org.activiti.engine.impl.form.BooleanFormType" />
    </list>
  </property>
</bean>
We need to add those custom form types (which are actually Activiti's standard form types), because we supplied our own class to process engine configuration in "formTypes" property and the default form types won't be registered (see ProcessEngineConfigurationImpl.initFormTypes() for more information).

Finally we can read our custom form property type and its config:
Task task = ...
TaskFormData taskFormData = formService.getTaskFormData(task.getId());
FormType formType = taskFormData.getFormProperties().get(0).getType();
assertThat(formType, instanceOf(SelectFromRangeFormType.class));
SelectFromRangeFormType select = (SelectFromRangeFormType) formType;
assertThat(select.getFrom(), is(0));
assertThat(select.getTo(), is(10));
Summary
To sum it up, adding custom form property types in Activiti is really easy. They can have any config parameters and, thanks to using <activiti:value> elements, we can use standard Activiti API to load those config parameters. A minor disadvantage is that we used Acitviti's internal classes (from *.impl.* packages), which may be changed in future version of Activiti.

The full source code for this post can be downloaded from my github account.