Taylor

Taylor Bpm

From Taylor

Taylor Bpm is a unique, event and rule based, process engine implemented with: Ejb3, Jpa, Jms, Seam, Expression Language, and Jsf.

It follows this simple pattern:

Action > Event > Condition > Action

  • Actions are invoked,
  • which generate Events,
  • which are evaluated for Conditions,
  • which in turn trigger more Actions.


Contents

Action > Event

Two sides of the same coin.

Actions are performed by users and automated processes. When these actions are performed an event is published to a JMS topic which contains information about the instance of the action.

  • Actions are implemented as operations on services
  • Services are implemented as seam based stateless session beans
  • The services are annotated with the net.taylor.event.interceptor.EventInterceptor which handles publishing events to the JMS topic
  • Events contain context information about the actions such as: service and operation name, inputs and outputs, etc
@Stateless
@Name("orderService")
@Interceptors({EventInterceptor.class})
public class OrderServiceBean implements OrderServiceLocal {
	...
	public Order submit(Order order) {
		order.setSubmitter(...);
		order.setStatus(Status.SUBMITTED);
		return entityManager.merge(order);
	}

	public Order notifySubmitter(Order order) {
		...
		return order;
	}
	...
}

This example shows that

  • the OrderService is annotated with the EventInterceptor
  • and the defined methods will generate events when invoked.

Event > Condition > Action

Processes are defined by rules which listen for events, evaluate conditions, and take further actions.

  • Processes are implemented as MDBs that subscribe to the event topic
  • Rules are implemented with annotations that define the conditions for when MDB operations are invoked
  • Expressions are used to access context variables and define conditions
  • The @Process annotation applies the WhenInterceptor which controls the processing
@MessageDriven(activationConfig = {
	@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),
	@ActivationConfigProperty(propertyName = "destination", propertyValue = "topic/net.taylor.EventTopic")})
@Name("orderProcess")
@Process
public class OrderProcessMDB extends ProcessMDB implements MessageListener {

	@When(invoked = @Invoked(service = OrderServiceBean.class, operation = "submit", 
		condition = "#{order.amount > 500}"))
	@InitiateTask(name = "Review Order", description = "Review: #{order.number}", 
		groups = "Reviewer", viewId = "/jsf/Order/View.xhtml")
	protected Task doReview(Order artifact) {
		return null;
	}

	@Asynchronous
	protected OrderServiceLocal orderService;

	@When(completed = @Completed(task = "Review Order", outcome = "Approve"))
	protected void doNofitySubmitter(@Arg("artifact") Order artifact) {
		orderService.notifySubmitter(artifact);
	}
	...
}

This example shows

  • the OrderProcess class with two rules:
  • the review step will be triggered when the OrderService submit operation is invoked and the order amount is greater than 500
  • the notify submitter step will be triggered when the Review Order task is completed with an outcome of Approve and call the Order Service notifySubmitter operation.
  • The process method arguments are set from the context.
    • See Expressions and Context below
    • doReview(Order artifact) uses the implicit expression #{order}
    • doNofitySubmitter(@Arg("artifact") Order artifact) uses the explicit expression #{artifact}
  • InitiateTask and Asynchronous are discussed below

Expressions, Context and the Pipeline

  • Expression Language is used to access context variables and define conditions
  • The context contains all of the inputs and outputs of the action that generated the event, plus any Seam component
  • The context also contains the pipeline or chain of events that lead to the current event
  • Special variable names:
    • event - the current event object (ex. #{event.output.number})
    • output - the action output (ex. #{output.number})
    • arguments[] - the action arguments (ex. #{arguments[0].number})
    • task - the completed task (ex. #{task.artifact.number})
    • artifact - the artifact of the task (ex. #{artifact.number})
    • root - the first event which started the process (ex. #{root.output.number})
    • lower camel case simple class name to search output, arguments, and artifact for an object instance of that type (ex. #{order.number})

All of these expression examples will return the same value. Another useful expression is #{root.userId} to determine who started the process.

Tasks

Some process steps require human interaction. These are called Tasks.

Initiate

Tasks are initiated by processes based on the @InitiateTask annotation.

	@InitiateTask(name = "Review Entity", 
			description = "Review: #{entity.name}", 
			groups = "Admin", 
			dueDate = @Time("#{entity.dueDate}"), 
			reminder = @Time(value = "#{entity.dueDate}", amount = -1, unit = Unit.Days),
			escalationGroup = "Manager",
			viewId = "/jsf/Entity/View.xhtml")
	protected Task doReview(Entity artifact) {
		return null;
		// optionally return partially initialized task(s) for special
		// customization
	}

This example shows

  • a task named Review Entity will be created,
  • the description will contain the name of the entity based on the expression,
  • the task will be assigned to the Admin role,
  • the entity instance will be attached to the task as the artifact, based on the doReview() argument,
  • and /jsf/Entity/View.xhtml will be used as the JSF view id when the task is accessed from the work list screen.
  • Due dates, reminders, and escalations are discussed below.

Worklist

Users access tasks from the Worklist screen. Tasks are only displayed in this screen if they apply to the current user.


buffer.append("t.status != net.taylor.worklist.entity.Status.COMPLETED and ");
buffer.append("t.status != net.taylor.worklist.entity.Status.CANCELLED and ");

builder.append("(");
builder.append("(t.user = :user) or ");
builder.append("(t.user is null and t.group in (:roles) and t.group2 is null and t.group3 is null) or ");
builder.append("(t.user is null and t.group in (:roles) and t.group2 in (:roles) and t.group3 is null) or ");
builder.append("(t.user is null and t.group in (:roles) and t.group2 in (:roles) and t.group3 in (:roles))");
builder.append(")");

The above code fragment shows

  • completed and canceled tasks are excluded
  • tasks assigned explicitly to the current user are included
  • tasks queued explicitly to the current user's roles are included
  • tasks can be queued based on a hierarchy of groups
    • ex. g1=role, g2=organization, g3=category
    • ex. customer service reps on the east coast that specialize in product returns
    • ex. groups = {"CustServRep", "#{request.customer.region}", "#{request.type}" }
  •  :user is assigned from the JAAS Subject
  •  :roles is assigned from the JAAS Subject

Worklist Actions

  • View - open the artifact
  • Edit - edit the task
  • Acquire - acquire the task from the queue/pool
  • Release - release the task back to the queue/pool
  • Suspend - suspend timers
  • Resume - resume timers
  • Cancel - cancel the task completely

Task View

Each task defines the JSF View Id that will be opened when View is selected from the Worklist. This can be any valid JSF view. By default the Taylor CRUD screens will display task related information when opened from the Worklist. This includes:

  • Task Name
  • Task Comments
  • Valid Task Actions
  • Bpm History

Another option is to associate a task with a view that has been modeled with a Taylor Pageflow diagram. In this case the pageflow will define transitions that correspond to the valid task actions.

Regardless, any view will leverage the generated task action component which is discussed next.

Task Action

  • TaskActions are used to interact with the current task and complete the task with the appropriate outcome
  • Each task has a handle to the artifact of interest which is inject to the task action from the currentTask
  • The @TaskAction annotation applies the TaskActionInterceptor which processes the invoked outcome operation based on the @CompleteTask annotation
  • A task action class is generated for each task node
@TaskAction
@Name("orderProcessReviewTaskAction")
@Scope(ScopeType.CONVERSATION)
public class ReviewTaskAction extends BaseTaskAction {

	@In
	protected Order artifact;
		
	@End
	@CompleteTask(outcome = "Approve")
	public String approve() {
		artifact.setStatus(Status.APPROVED);
		return Worklist.OUTCOME;
	}

	@End
	@CompleteTask(outcome = "Reject")
	public String reject() {
		artifact.setStatus(Status.REJECTED);
		return Worklist.OUTCOME;
	}
}

The following are task related factory components that can be used from any view.

  • #{currentTask} - the current task selected from the worklist
  • #{artifact} - the artifact attached to the currentTask
  • #{currentTaskAction} - the action component defined by the currentTask which provides appropriate actions for completing the currentTask
  • #{taskCommentAction} - a component for adding comments to the currentTask

Notification

Tasks support Email Notifications.

The TaskNotificationMDB subscribes to all Task events and sends out emails to assignees and groups.

The following task actions/events trigger email notifications:

  • initiate
  • complete
  • cancel
  • release
  • timeoutReminder
  • timeout
  • suspend
  • resume

The Form field can be overridden using a resource bundle:

  • task.from.name=task-manager
  • task.from.email=do-not-reply@workflow

The To fields is set from the task data:

  • task.user or
  • intersection of task.group, task.group2, and task.group3
  • plus, if timeout then intersection of task.escalationGroup, task.group2, and task.group3

Templates

  • The emails are defined uses Facelets
  • The default template is named: task-email.xhtml
  • Templates can be overridden per task based on a naming convention.
  • ex. For the initiation of the Review Order task the following names would be used in this order:
    • review-order-initiate-email.xhtml
    • review-order-email.xhtml
    • task-initiate-email.xhtml
    • task-email.xhtml (default)

Timers

Time

The Time annotation is used to specify a point in time for due dates, reminders, and expirations.

  • value - An expression for retrieving the time.
  • unit - Used with amount() to calculate the time from now or from the result of the expression. The Unit enum is a wrapper around the appropriate Calendar constants: Seconds, Minutes, Hours, Days, Weeks, Months, Years.
  • amount - Used with unit() to calculate the time from now or from the result of the expression. Can be a positive or negative integer.

Value can be used alone, or just Unit and Amount can be used, or all three can be used together.

Task Escalation

Tasks have a built in escalation feature.

  • Reminders will send out an email to the assignees at the designated time.
  • On the Due Date the task will send out a final email notification.
    • If an escalation role is specified then the task will be escalated.
    • If a timeout transition is specified then it will be followed.
  • These are implemented with an Ejb Timer.
	@InitiateTask(name = ReviewTaskAction.NAME, 
			description = "Review: #{entity.name}", 
			groups = "Admin", 
			dueDate = @Time("#{entity.dueDate}"), 
			reminder = @Time(value = "#{entity.dueDate}", amount = -1, unit = Unit.Days),
			escalationGroup = "Manager",
			viewId = "/jsf/Entity/View.xhtml")

This example shows:

  • The due date is implemented by a method on the entity. This could be a value stored in the database or an arbitrarily complex calculation.
  • The reminder is sent out 1 day before the due date.
  • The task will be escalated to the Manager role.

Wait

Processes can have a Timer node that waits for a timer to expire.

  • When the expiration time is reached an event is published to trigger the next step(s).
  • These are implemented with an Ejb Timer.
	protected static final String TIMER_NAME = "TimerTest";
	
	@When(invoked = { @Invoked(service = SampleServiceBean.class, operation = "submit") })
	@InitiateTimer(name = TIMER_NAME, expiration = @Time(amount = 5, unit = Unit.Seconds))
	protected Date doTimer(Entity entity) {
		// NOTE: this is an alternate way to override the default behavior to
		// create a date/time with an advanced calculation
		return DateUtil.addSeconds(5);
	}

	@When(expired = { @Expired(name = TIMER_NAME) })
	protected void doExpired(Entity entity) {
		sampleService.expired(entity);
	}

This example shows:

  • The timer will wait for 5 seconds from the time it is started.
  • The expiration event will trigger the call to the sample service expired operation.

Scheduled

Scheduled timers are used to start new instances of processes.

  • They are models as a timer with the Scheduled stereotype with a transition to the Invoke step that defines the operation to be called by the job.
  • The Scheduled stereotype provides for setting the Cron expression.
  • These are implements as Quartz Jobs
@MessageDriven(activationConfig = { 
	@ActivationConfigProperty(propertyName = "cronTrigger", propertyValue = "0 0 2 * * ?") })
@ResourceAdapter("quartz-ra.rar")
public class OrderProcessJob implements Job {
	
	@EJB
	private OrderServiceLocal service;

	public void execute(JobExecutionContext ctx) throws JobExecutionException {
		service.initiateImport();
	}
}

Fork and Join

	protected static final String SUBMIT_FORK = "submit";

	@When(invoked = { @Invoked(service = SampleServiceBean.class, operation = "submit") })
	@Fork(name=SUBMIT_FORK)
	protected void doFork(Entity entity) {
	}

	@When(invoked = {
			@Invoked(service = SampleServiceBean.class, operation = "concurrent1"),
			@Invoked(service = SampleServiceBean.class, operation = "concurrent2") })
	@Join(name=SUBMIT_FORK, count = 2)
	protected void doJoined(Entity entity) {
		// NOTE: this is not invoked until the count is met
		sampleService.joined(entity);
	}

Asynchronous Invocation

When a process MDB receives an event it will trigger all the actions for which the conditionals evaluate true.

  • For performance reasons we want these actions to run concurrently.
  • For quality of service reasons we want each action to run independent of each other, so that one can fail without the others.

To achieve this concurrency and isolation each action is run asynchronously.

  • When the @Asynchronous annotation is used an asynchronous dynamic proxy will be injected by the AsynchronousInterceptor.
	@Asynchronous
	private OrderServiceLocal orderService;
  • The asynchronous dynamic proxy will place the request on the net.taylor.RequestQueue to be processed by the RequestProcessorMDB.

Retry and Resubmit

When the RequestProcessorMDB encounters an exception it will attempt to retry the request. This logic is provided by the RetryInterceptor.

  • It will delay before retrying based on the queue configuration, see RedeliveryDelay below
<mbean code="org.jboss.jms.server.destination.QueueService"
	name="jboss.messaging.destination:service=Queue,name=net.taylor.RequestQueue"
	xmbean-dd="xmdesc/Queue-xmbean.xml">
	<depends optional-attribute-name="ServerPeer">jboss.messaging:service=ServerPeer</depends>
	<depends>jboss.messaging:service=PostOffice</depends>
	<attribute name="RedeliveryDelay">30000</attribute>
</mbean>
  • It will only retry a maximum number of times, see DLQMaxResent below
@MessageDriven(activationConfig = {
	@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
	@ActivationConfigProperty(propertyName = "destination", propertyValue = "queue/net.taylor.RequestQueue"),
	@ActivationConfigProperty(propertyName = "DLQMaxResent", propertyValue = "3"),
	@ActivationConfigProperty(propertyName = "subscriptionDurability", propertyValue = "Durable") })
@Name("requestProcessorMDB")
@Interceptors(RetryInterceptor.class)
public class RequestProcessorMDB implements MessageListener {
	...
}

If the maximum number of retries is reached then it will publish a Fault event.

The FaultHandlerMDB listens for these events and assigns a task to the Admin role. Once the cause of the error has been resolved the Admin can resubmit the request by completing the task.

Testing

Unit Testing

net.taylor.bpm.testing.AbstractBpmTestCase.java is the base class for testing processes.

A test class is generated for each process with a test for every possible flow.

Methods of interest:

  • completeTask(WAIT, name, outcome, user, password)
    • Wait for and complete a task with the specified outcome
    • User and password are used to determine the user's roles
  • assertEventCount(WAIT, artifact, count);
    • Wait for the specified time and assert the event count
    • To ensure that each test flow is independent the deleteAllTasksAndEvents() method should be called in setup()
  • waitForEventsToProcess(WAIT)
    • Since processes are asynchronous it is necessary for the test to wait a sufficient amount of time for the processing to occur. completeTask() and assertEventCount() have this feature included. This method allows for adding additional wait time.
  • Event publication can be enabled/disabled using EventUtil.setDisableForUnitTesting(true). This is useful for testing services that have the EventInterceptor without triggering the instantiation of processes.

Timers

Timers are problematic for testing because they are usually defined in terms of days. Therefore it is necessary to override the time unit for testing.

The Bpm Configuration MBean provides this ability. It can be accessed pragmatically or through a JMX Console.

  • Configuration.getMBean().setUnitTesting(true)
    • Overrides the unit to seconds.
  • Configuration.getMBean().setIntegrationTesting(true)
    • Overrides the unit to minutes.

For unit testing it is possible to completely override the value of the timer.

  • addTimerOverrides(process, timer name, date value or null)

Additional 1.4.0 Features

  • Configuration.getMBean().setTaskTimersEnabled(false)
    • Enable and disable task timers for development and testing.
  • Configuration.getMBean().setTimersExpireImmediately(true)
    • Configure timers to expire immediately for development and testing.
  • Use /jsf/bpm/TimerOverrides.seam to override individual timers online.


TODO

  • components.xml configuration ???
  • taylor-results scripting ???

TODO

  • Monitor
  • History
  • Admin
  • Management
  • Screen Shots
  • discuss modal/stateful vs modeless/stateless process model
  • discuss status flow alternative

Related