Jan Amoyo

on software development and possibly other things

Flowee: Sample Application

No comments
My previous post introduced Flowee as a framework for building Java services backed by one or more workflows. Through a sample application, this post will demonstrate how easy it is to build workflow-based services using Flowee.

The sample application is a service which authenticates two types of accounts: an admin and a user. The service will display a greeting then authenticate each type of account using different authentication methods.
Figure 1: Sample Workflow Service
Note that this example is by no means a realistic use-case for production, it is only used here for illustration purposes.

Implementing the Service

(1) We start by defining the request:
public class LoginRequest {
  private String username;
  private String password;
  private String type;
  // Accessors omitted
}

(2) We also create an application specific WorkflowContext (this is an optional step, the default workflow context is probably sufficient for some applications):
public class LoginContext extends WorkflowContext {
  private static final String KEY_IS_AUTHENTICATED = "isAuthenticated";

  public void setIsAuthenticated(Boolean isAuthenticated) {
    put(KEY_IS_AUTHENTICATED, isAuthenticated);
  }

  public Boolean getIsAuthenticated() {
    return (Boolean) get(KEY_IS_AUTHENTICATED);
  }
}
Here, we extend the default context with convenience methods to access the value mapped to the "isAuthenticated" key (return values are normally stored in the context).

(3) Then we define an application specific Task interface to remove generic types:
public interface LoginTask
  extends Task<LoginRequest, LoginContext> {
}

(4) Next, we define an application specific abstract Task. We make it BeanNameAware so that the tasks assumes the bean name if declared in Spring:
public abstract class AbstractLoginTask
    extends AbstractTask<LoginRequest, LoginContext>
    implements LoginTask, BeanNameAware {
  @Override
  public void setBeanName(String name) {
    setName(name);
  }
}

(5) We can now create an application specific Workflow:
public class LoginWorkflow
    extends AbstractWorkflow<LoginTask, LoginRequest, LoginContext> {
  public LoginWorkflow(String name) {
    super(name);
  }
}
The abstract implementation should provide all the functionality we need, this step is to simply to declare the generic parameters.

(6) Next, we define a WorkflowFactory. For this example, we will make our workflow factory configurable from properties files. To achieve this, we need to inherit from AbstractPropertiesWorkflowFactory:
public class LoginWorkflowFactory
    extends AbstractPropertiesWorkflowFactory <LoginWorkflow, LoginTask, LoginRequest, LoginContext> {
  @Override
  protected LoginWorkflow createWorkflow(String name) {
    return new LoginWorkflow(name);
  }
}
This requires us override createWorkflow(), an Abstract Factory method.

(7) We then define the Filter that will be used by our configurable workflow factory. Recall from my previous post that configurable factories uses filters to evaluate conditions that determine which workflows gets created.

Flowee comes with an abstract implementation that evaluates conditions as JEXL expressions. Using JEXL allows us to define JavaScript-like conditions for our workflow configuration:
public class LoginFilter
    extends AbstractJexlFilter<LoginRequest, LoginContext> {
  @Override
  protected ReadonlyContext populateJexlContext(LoginRequest request,
      LoginContext context) {
    JexlContext jexlContext = new MapContext();
    jexlContext.set("request", request);
    return new ReadonlyContext(jexlContext);
  }
}
The method, populateJexlContext() populates a JEXL context with the LoginRequest. This allows us to access fields and methods of the request using JEXL expressions (ex: request.type == 'admin' ).

(8) We now have everything we need to define the WorkflowService:
public class LoginService
    extends AbstractWorkflowService<LoginWorkflow, LoginTask, LoginRequest, LoginContext> {
  @Override
  protected LoginContext createContext() {
    return new LoginContext();
  }
}
Here, we override an Abstract Factory method for creating an instance of the LoginContext.

Implementing the Tasks

Now that we have the infrastructure for our workflow service, the next stage is to define the actual tasks that comprise the workflows.

(1) We create a simple task that greets the user being authenticated:
public class GreetUserTask extends AbstractLoginTask {
  @Override
  protected TaskStatus attemptExecute(LoginRequest request,
      LoginContext context) throws WorkflowException {
    System.out.println(String.format("Welcome '%s'!",
        request.getUsername()));
    return TaskStatus.CONTINUE;
  }
}

(2) We then define the task which authenticates admin accounts
public class AuthenticateAdmin extends AbstractLoginTask {
  @Override
  protected TaskStatus attemptExecute(LoginRequest request, LoginContext context) throws WorkflowException {
    if ("admin".equals(request.getUsername()) && "p@ssw0rd".equals(request.getPassword())) {
      System.out.println(String.format("User '%s' has been authenticated as Administrator",
          request.getUsername()));
      context.setIsAuthenticated(Boolean.TRUE);
      return TaskStatus.CONTINUE;
    } else {
      System.err.println(String.format("Cannot authenticate user '%s'!",
          request.getUsername()));
      context.setIsAuthenticated(Boolean.FALSE);
      return TaskStatus.BREAK;
    }
  }
}
Normally, this task should perform authentication against a data source. For this example, we are only trying to simulate a scenario where admin and user accounts are authenticated differently.

(3) We then define the task which authenticates user accounts
public class AuthenticateUser extends AbstractLoginTask {
  @Override
  protected TaskStatus attemptExecute(LoginRequest request, LoginContext context)
      throws WorkflowException {
    if ("user".equals(request.getUsername())
        && "p@ssw0rd".equals(request.getPassword())) {
      System.out.println(String.format("User '%s' has been authenticated",
          request.getUsername()));
      context.setIsAuthenticated(Boolean.TRUE);
      return TaskStatus.CONTINUE;
    } else {
      System.err.println(String.format("Cannot authenticate user '%s'!",
          request.getUsername()));
      context.setIsAuthenticated(Boolean.FALSE);
      return TaskStatus.BREAK;
    }
  }
}

Spring Integration

We now have all the components we need to build the application. It's time to wire them all in Spring.

(1) We start with the tasks. It is a good practice to provide a separate configuration file for tasks, this makes our configuration manageable in case the number of tasks grows.
<beans>
  <bean id="greet" class="com.jramoyo.flowee.sample.login.task.GreetUserTask" />
  <bean id="authenticate_user" class="com.jramoyo.flowee.sample.login.task.AuthenticateUser" />
  <bean id="authenticate_admin" class="com.jramoyo.flowee.sample.login.task.AuthenticateAdmin" />
</beans>
Note that we are not wiring these tasks to any of our components. Flowee Spring Module (flowee-spring) comes with an implementation of TaskRegistry that looks-up task instances from the Spring context.

(2) We then define our main Spring configuration file.
<beans>
  <import resource="classpath:spring-tasks.xml" />
  <bean id="workflowFactory" class="com.jramoyo.flowee.sample.login.LoginWorkflowFactory">
    <property name="filter">
      <bean class="com.jramoyo.flowee.sample.login.LoginFilter" />
    </property>
    <property name="taskRegistry">
      <bean class="com.jramoyo.flowee.spring.ContextAwareTaskRegistry" />
    </property>
  </bean>
  <bean id="workflowService" class="com.jramoyo.flowee.sample.login.LoginService">
    <property name="factory" ref="workflowFactory" />
  </bean>
</beans>
Here, we declare our LoginFactory as a dependency of LoginService. LoginFactory is then wired with LoginFilter and ContextAwareTaskRegistry. As mentioned in the previous step, ContextAwareTaskRegistry allows our factory to look-up task instances from the Spring context.

Workflow Configuration

The last stage is to configure the WorkflowFactory to assemble the workflows required for an account type.

Recall from the previous steps that we using an instance of AbstractPropertiesWorkflowFactory. This loads configuration from two properties files: one for the workflow conditions (workflow.properties) and another for the workflow tasks (tasks.properties).

(1) We create the workflow.properties file with the following content:
admin=request.type == 'admin'
user=request.type == 'user'
This configuration means that if the value of LoginRequest.getType() is 'admin', execute the workflow named 'admin' and if the value is 'user' execute the workflow named 'user'

(2) Then, we create the task.properties file:
admin=greet,authenticate_admin
user=greet,authenticate_user
This configures the sequence of tasks that make up the workflow.

By default, both workflow.properties and task.properties are loaded from the classpath. This can be overridden by setting setWorkflowConfigFile() and setTaskConfigFile() respectively.

That's all there is to it! From Spring, we can load LoginService and inject it anywhere within the application.

Notice that while we created several components to build our service, most of them were simple inheritance and required very little lines of code. Also, we only need to build these component once per service.

You'll find that the value of Flowee becomes more evident as the number of workflows and tasks increases.

Testing

We create a simple JUnit test case:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:/spring.xml")
public class LoginServiceTest {

  @Resource(name = "workflowService")
  private LoginService service;

  @Test
  public void testAdminLogin() throws WorkflowException {
    LoginRequest request = new LoginRequest();
    request.setUsername("admin");
    request.setPassword("p@ssw0rd");
    request.setType("admin");

    LoginContext context = service.process(request);
    assertTrue("Incorrect result",
      context.getIsAuthenticated());

    request.setPassword("wrong");
    context = service.process(request);
    assertFalse("Incorrect result",
      context.getIsAuthenticated());
  }

  @Test
  public void testUserLogin() throws WorkflowException {
    LoginRequest request = new LoginRequest();
    request.setUsername("user");
    request.setPassword("p@ssw0rd");
    request.setType("user");

    LoginContext context = service.process(request);
    assertTrue("Incorrect result",
      context.getIsAuthenticated());

    request.setPassword("wrong");
    context = service.process(request);
    assertFalse("Incorrect result",
      context.getIsAuthenticated());
  }
}
Notice how we injected LoginService to our unit test.

Our test yields the following output:
Welcome 'admin'!
User 'admin' has been authenticated as Administrator

Welcome 'admin'!
Cannot authenticate user 'admin'!

Welcome 'user'!
User 'user' has been authenticated

Welcome 'user'!
Cannot authenticate user 'user'!

The code used in this example is available as a Maven module from Flowee's Git repository.

So far, I've only demonstrated simple workflows with linear task sequences. My next post will introduce special tasks which allows for more complex task sequences.

No comments :

Post a Comment

Introducing Flowee, a Framework for Building Workflow-based Services in Java

No comments
Overview

My past roles required me to write three different applications having surprisingly similar requirements. These requirements were:
  1. The application must run as a service which receives some form of request
  2. Once received, several stages of processing needs to be performed on the request (i.e. validation, persistence, state management, etc)
  3. These stages of processing may change overtime as new requirements come in
The third requirement is arguably the most important. While the first two can be performed without special techniques or design, the last requirement requires a bit of planning.

In order to achieve this required flexibility, adding new processing logic or modifying existing ones should not affect the code of other tasks. For example: if I add a new logic for validation, my existing code for state management should not be affected.

I need to encapsulate each logic as individual units of "tasks". This way, changes to each task is independent from the other.

The behavior of my service is now defined by the sequence of tasks needed to be performed. This behavior can easily be changed by adding, removing, or modifying tasks in the sequence.

Essentially, I needed to build a workflow.

Design

My design had several iterations throughout the years. My initial design was pretty straightforward: I implemented a workflow as a container of one or more tasks arranged in a sequence. When executed, the workflow iterates through each task and executes it. The workflow has no knowledge of what each task would do, all it knows is that it is executing some task. This was made possible because each task share a common interface. This interface exposes an execute() method which accepts the request as a parameter.

Through dependency injection, the behavior of the workflow becomes very easy to change. I can add, remove and rearrange tasks via configuration.

While proven to be effective, my initial design was application specific -- the workflow can only accept a specific type of request. This makes it an ineffective framework because it only works for that particular application. There was also the problem of tasks not being able to share information: as a result, temporary values needed to be stored within the request itself.

I had the chance to improve on this design on a later project. By applying Generics, I was able to redesign my workflow so that it can accept any type of request.

In order for information to be shared across each tasks, a data structure serving as the workflow "context" is also passed as a parameter to each task.
Figure 1: Workflow and Tasks

More often than not, different workflows need to be executed for different types of request. For example, one workflow needs to be executed to open an account, another workflow to close an account, etc.

I came up with a "workflow factory" as a solution to this requirement. Depending on the type of request, the factory will assemble the workflows required to be executed against the request. The factory is exposed as an interface so that I can have different implementations depending on the requirement.
Figure 2: Workflow Factory

My service now becomes very simple: as soon as a request comes in, I will call the factory to assemble the required workflows then one-by-one execute my request against each of them.
Figure 3: Workflow Service

Flowee Framework

Flowee is an open source implementation of the above design. Having gone through several iterations, I feel that this design has matured enough to warrant a common framework that can be useful to others.

The project is hosted via GitHub at https://github.com/jramoyo/flowee.

Workflow and Tasks

The core framework revolves around the Workflow interface and its relationship with the Task interface.
Figure 4: Flowee Workflow

The execute() method of Workflow accepts both the request and an instance of WorkflowContext. Errors encountered while processing the request are thrown as WorkflowExceptions.

AbstractWorkflow provides a generic implementation of Workflow. It iterates through a list of associated Tasks to process a request. Application specific workflows will inherit from this class.

Most workflow Tasks will inherit from AbstractTask. It provides useful features for building application specific tasks namely:
  • Retry task execution when an exception is encountered
  • Silently skip task execution instead of throwing an exception
  • Skip task execution depending on the type of request

WorkflowFactory

The WorkflowFactory is another important piece of the framework. The way it abstracts the selection and creation of workflows simplifies the rest of the core components.
Figure 5: Flowee WorkflowFactory

AbstractConfigurableWorkflowFactory is used to build configuration  driven workflow factories. It defines an abstract fetchConfiguration() method and an abstract fetchTaskNames() method that sub-classes need to implement. These methods are used to fetch configurations from various sources: either from the file system or from a remote server.

The configuration is represented as a Map whose key is the name of a workflow and whose value is the condition which activates that workflow.

AbstractConfigurableWorkflowFactory uses a Filter instance to evaluate the conditions configured to activate the workflows.

AbstractPropertiesWorkflowFactory is a sub-class of AbstractConfigurableWorkflowFactory that fetches configuration from a properties file.

WorkflowService

WorkflowService and AbstractWorkflowService acts as a facade linking the core components together.
Figure 6: Flowee WorkflowService
With all the complexities taken care of by both the Workflow and the WorkflowFactory, our WorkflowService implementation becomes very simple.

While most applications will interact with workflows through the WorkflowService, those requiring a different behavior can interact with the underlying components directly.

Conclusion

The primary purpose of Flowee is to provide groundwork for rule-driven workflow selection and execution. Developers can focus the majority of their efforts on building the tasks which hold the actual business requirements.

Workflows built on Flowee will run without the need for "containers" or "engines". The framework is lightweight and integrates seamlessly with any application.

This post discussed the design considerations which led to the implementation of Flowee. It also described the structure of the core framework. My next post will demonstrate how easy it is to build workflow-based services with Flowee by going through a sample application.

No comments :

Post a Comment