Copyright © 2003,2004 OpenBR Sistemas S/C Ltda.
Abstract
This HOWTO is a hands-on overview of the Bossa API. Use it as a starting point on the use of Bossa, before going to the more detailed API reference in the javadocs.
Table of Contents
Bossa is a workflow engine written in Java designed to be embedded. Therefore, Bossa is organized as a library to be used by server side applications (especially web oriented ones) that need workflow services.
This HOWTO is a hands-on overview of the Bossa API. It was created as a starting point on the use of Bossa, less complete than the more detailed API reference but giving a practical overview of the API. The comprehensive API reference can be found in the javadocs and for an explanation of Bossa's general design and philosophy see the Bossa Manifesto.
Starting the Bossa engine is very simple, it is only necessary to create an object of class Bossa using the factory class BossaFactory. The Bossa library is organized in a way that you can access all methods starting from the Bossa object. You can have more than one instance of this object, each one representing a different Bossa engine. Example 1 shows how to create a new Bossa engine.
The static method defaultBossa will create a Bossa engine with all configuration parameters set to the default. To change a configuration parameter, you will need to create a BossaFactory, use its methods to set up the Bossa engine and finally create it using the createBossa method. Example 2 shows how to change the name of the directory where Bossa will store its state.
Example 2. Creating a configured Bossa engine.
BossaFactory factory = new BossaFactory(); factory.setStateDir("/var/lib/bossa"); Bossa bossa = factory.createBossa();
Bossa only needs a writable directory to persist its data, check if the application has the appropriate permissions. If the provided directory is empty, Bossa will start a new engine instance, otherwise it will automatically reload the state of the last execution.
Keep the reference to the just created Bossa engine and pass it to the objects that will need to interact with it. Do not create more the one engine instance pointing to the same state directory.
Before we can start managing case types, it is necessary to create a case type. In Bossa a case type is represented by an object of the CaseType class. Example 3 shows how to create the "supplies purchase" case type presented in the Bossa Manifesto.
Example 3. Creating a new case type.
CaseType caseType = new CaseType("TestCaseType"); Place A = caseType.registerPlace("A", 1); Place B = caseType.registerPlace("B"); Place C = caseType.registerPlace("C"); Place D = caseType.registerPlace("D"); Place E = caseType.registerPlace("E"); Place F = caseType.registerPlace("F"); Place G = caseType.registerPlace("G"); Place H = caseType.registerPlace("H"); Transition a = caseType.registerTransition("a", "requesters"); Transition b = caseType.registerTransition("b", "sales-$a"); Transition c = caseType.registerTransition("c", "directors"); Transition d = caseType.registerTransition("d", "sales"); Transition e = caseType.registerTransition("e", "sales"); Transition f = caseType.registerTransition("f", "$a"); a.input(A, "1"); a.output(B, "1"); b.input(B, "1"); b.output(C, "!SOK"); b.output(D, "SOK && DIR"); b.output(E, "SOK && !DIR"); c.input(D, "1"); c.output(B, "ADIR == 'BACK'"); c.output(E, "ADIR == 'OK'"); c.output(H, "ADIR == 'CANCEL'"); d.input(E, "1"); d.output(F, "1"); e.input(F, "1"); e.output(G, "1"); f.input(C, "1"); f.output(B, "OK"); f.output(H, "!OK"); HashMap attributes = new HashMap(); attributes.put("SOK", new Boolean(false)); attributes.put("DIR", new Boolean(false)); attributes.put("ADIR", ""); attributes.put("OK", new Boolean(false)); caseType.buildTemplate(attributes);
First we create an empty case type, passing to the CaseType constructor a string id. Then we create all places and transitions of this case type using the methods registerPlace and registerTransition of the newly created case type.
Both methods receive an id as parameter, this id can be used to later recover the just created places and transitions. The registerPlace method receives also an optional initial state, indicating how many tokens are present in this place when a new case begins. If the initial state is not indicated, the place will start with zero tokens. Additionally, the registerTransition method receives a resource expression, indicating which resources can perform work items reflecting this transition.
After creating the places and transitions, we must connect them. For each transition we add inputs and outputs using the methods input and output, respectively. Each of these methods receives the place that stands on the other side of the edge and the edge weight. The edge weight is given as a JavaScript integer or boolean expression.
The last step in the creation of a case type involves the creation of the template case. This case represents the starting state of all cases created from this case type. The main part of this state is already set by the initial token placement made when the places were created. But, to create the template, we need also to initialize all of its case variables. The case variables are set in a Map where a basic java type is indexed by the variable name.
Now that we have a case type, we can register it. The case type management operations of the Bossa engine are accessed though the CaseTypeManager object that is part of the engine. Example 4 shows how to get the CaseTypeManager object and how to register a case type.
Example 4. Registering a case type.
CaseTypeManager caseTypeManager = bossa.getCaseTypeManager(); caseTypeManager.registerCaseType(caseType);
After a case type is registered, it is possible to retrieve it from the case type manager using the method getCaseType, this will be useful when we associate local resources with case type resources.
In Bossa, resource management has two aspects: system resource maintenance and case type resource association. All resource management operations of the Bossa engine are accessed though the ResourceManager object that is part of the engine. We will start by showing how to get a resource manager an how to create and manage system resources in Example 5.
Example 5. Managing system resources.
ResourceManager resourceManager = bossa.getResourceManager(); Resource joe = resourceManager.createResource("joe"); Resource mary = resourceManager.createResource("mary"); Resource ana = resourceManager.createResource("ana"); Resource people = resourceManager.createResource("people"); Resource managers = resourceManager.createResource("managers"); people.include(joe); people.include(mary); people.include(ana); managers.include(mary); managers.include(ana);
To the Bossa engine resources are no more than the string id that identifies them. You must create one resource for each user of the system using the createResource method and must use an id that allows you to retrieve the resource later with the getResource method. Resources can be nested, an empty resource is an user and a resource that contains other resources is a group. To Bossa there is no difference between a single resource or a group, they are all resources.
You add a resource to another with the method include of Resource. The Resource class provides other methods that make it possible to create advanced access control lists, see its documentation. Finally, you can remove a resource from the resource manager using the removeResource method.
Besides the system resources, each case type has a set of private resources. These resources are extracted from the resource expressions and are meant to represent groups, even though they start empty. For example, the resource expression "sales + support" in a transition will create the resources "sales" and "support" in the case type. To allow any system resource to be able to perform the work item represented by this transaction, you must add system resources into the case type resources. You do this the same way as you handle nested system resources, as Example 6 shows.
Example 6. Associating system and case type resources.
List l = caseTypeManager.getCaseType("TestCaseType").getResources(); // Find resources named requesters, sales and directors in l. // Your user will probably "find" the resources for you in an GUI. requesters.include(people); sales.include(managers); directors.include(ana);
Now that we have all set up, it possible to get work item lists for users and to perform these work items. The component of the Bossa engine that handles work item lists is the WorkManager object. Example 7 shows how to get the work manager and a list of work items.
Example 7. Getting a work item list.
WorkManager workManager = bossa.getWorkManager(); Resource resource = resourceManager.getResource("joe"); List l = workManager.getWorkItems(resource, true);
The second argument of the getWorkItems method is a boolean value that allows initial work items to be included in the list. Initial work items are special work items that, if performed, will create a new case. In Bossa we don't create a case explicitly, but only through initial work items. Despite being special, initial work items work the same way as normal work items.
With the list of WorkItem objects we can present them to the user or use some form of automatic selection and pick a work item. We can now start the work item by opening it, as Example 8 shows.
Example 8. Opening a work item.
// Find the desired work item in l and put it in workItem. Activity activity = workItem.open(resource);
The open method of WorkItem receives the resource opening the work item and returns an Activity object. That is, an open work item is an activity. Now the resource has an activity locked to him and he can now finish it, by either closing or canceling this activity. Example 9 shows the two possibilities.
Example 9. Finishing an activity.
if (/* Do I want to close it? */) { HashMap attributes = new HashMap(); attributes.put("SOK", new Boolean(true)); attributes.put("DIR", new Boolean(false)); activity.close(attributes); } else { activity.cancel(); }
When an activity is effectively closed, we have the option of updating the case variables. This is done the same way as when creating the case type, and the Map is passed as a parameter to the close method. If an activity is closed the Bossa engine will register that by moving the tokens of the effected case, this will eventually activate other work items and the case will progress. If the activity is canceled, the case will return to the state it was before the work item was opened.
The comprehensive Bossa API reference can be found in the javadocs. Also, there is in the source distribution a demo class that implements a crude text based interface. It is found in the testcases source directory and is called BossaBrowser. The source code of this demo shows how you can structure an application using Bossa, but beware, the code is not of the same high standard as the library.
However, the real place to get unambiguous, direct and clear information about Bossa is the source code. We hope the source is sufficiently well structured and commented to be of some use. If not ;), please ask us at the project mailing list.