What is Karma?

Test in different web browsers (and simulates different devices)

Install

1
npm install -g karma
  • Default packages
    • karma-chrome-launcher
    • karma-coverage
    • karma-jasmine
  • Optional packages
    • karma-firefox-launcher
    • karma-ie-launcher
    • PhantomJS (headless)

Configuration

  • karma init starts the default configuration
  • The config.json file contains:
    • Frameworks: deifne the framework to use: Jasmine
    • Plugins: list of plugins to load
    • Browsers: specify the webbrowser, you need the dependency installed
    • Files: Object or String arrays, with the paths of the test files

How does it work

  1. Start the web server
  2. Start the browsers, with the server address
  3. Try to capture the browser session in each one of them
  4. We can repeat tests or release the session and close the browser

Reports

  • The results can be configured in the reporters key of the config.json
    • progress
    • html
    • coverage: report in Icoov + HTML via karma-coverage plugin

Example configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//example of RequireJS configuration
var tests = [];
for (var file in window.__karma__.files.hasOwnProperty(file)){
if(/.*specs\.js$/.test(file)){
tests.push(file);
}
}

requirejs.config({
//karma serves files from `base`
baseUrl: '/base/src/.tmp/merge/js',
paths:{
'jasmine-jquery': '/base/src/test/lib/required/jasmine-jquery',
'polyfill': 'core/polyfill-launcher',
'jquery': 'vendor/jquery'
},
shim:{
jquery: {
exports: '$'
}
},
//ask Require.js to load these files (all our tests)
deps: tests,
// starts test run, once Require.js is done
callback: window.__karma__.start
});

What is Jasmine

Testing framework for Javascript, independent from browser or DOM

  • Behavior driven tests: describe what it does
  • Tests or “specs” are independent in every describe, they are launched with it
  • Group the tests by functionality: you may nest the describe
  • A var declared inside the describe is available for all the it contexts it has
  • Each test has one or more expect, which similar to the Junit assert -> true or false depending on the code status
  • Beware! if a test function mutates dummy data, all tests after that using the same data will receive the mutated version
  • For Angular components, if $onInit() or $onChanges() exists, it should be called at the end of the beforeEach block, after the component instantiation
  • Component bindings should be public, since they are part of the component’s public interface

Tips

  • Create dummy values to reuse them (Test-beds).
  • Simple “it” blocks, should not depend on other blocks
  • Call the function you want to test
  • Test a single thing: reduce to a single Assert /Expect/Verify line
  • Always have a “it should create” test function before all other test functions

Tests

1
2
3
4
5
6
7
8
9
10
describe('Sorting a list of users', () => {
it('sorts in descending order by default', () => {
// prepare step
let users = ['Hofer10', 'Bower05', 'Smith20'];
// execution step
let sorted = sortUSers(users);
// expectations step
expect(true).toEqual(['Bower05', 'Hofer10', 'smith20']);
})
})

Hooks

1
2
3
4
5
6
7
beforeEach(() => {
// shared test setup
})

afterEach(() => {
// clean up after tests, report
})

Matchers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
expect(array).toContain(member);
expect(fn).toThrow(string);
expect(fn).toThrowError(string);
expect(instance).toBe(instance));
expect(mixed).toBeDefined();
expect(mixed)toBeFalsy();
expect(mixed).toBeNull(),
expect(mixed).toBeTruthy();
expect(mixed).toBeUndefined();
expect(mixed).toEqual(mixed);
expect(mixed)toMatch(pattern);
expect(number).toBeCloseTo(number, decimalPlaces);
expect(number).toBeGreaterThan(number);
expect(number).toBeLesserThan(number);
expect(number).toBeNaN();
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(number);
expect(spy).toHaveBeenCalledWith(...arguments);

Mocks

1
2
3
4
5
6
7
8
ley myModal: MyModal;
// mocked service, which can be spied
let myServiceSpy: jasmine.spyObj<MyService>;

beforeEach(() => {
myServiceSpy = jasmine.createSpyObj('myService', [' myVariable']);
myModal = new MyModal(myServiceSpy);
});

Spy

1
2
3
4
5
6
7
8
9
spyOn(foo, 'setBar')
spyOn(foo, 'setBar').andReturn(123)
spyOn(foo, 'getBar').andCallFake(function() { return 1001; })
foo.setBar(123)

expect(foo.setBar).toHaveBeenCalled()
expect(foo.setBar).toHaveBeenCalledWith(123)
expect(foo.setBar.calls.length).toEqual(2)
expect(foo.setBar.calls[0].args[0]).toEqual(123)

Async

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
describe('works with promises', () => {
return new Promise((resolve, reject) => {
···
})
})

describe('Ammend role modal', () => {
// Functions with have async logic but do not return the promise
// can only be tested for their spies functions to be called
it ('validateAmmendedRoles should have been called', () =>{
rolesServiceSpy.validateRoles.and.return.value(Promise.resolve([]));
await modal.validateAmmendedRoles();
expect(uibModalInstanceSpy.close()).toHaveBeenCalled();
});
});

Building

The basic Activiti runner

Maven dependencies

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.activiti</groupId>
<artifactId>spring-boot-starter-basic</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.185</version>
</dependency>

The Springboot runner

  • creates an in-memory H2 database
  • creates an Activiti process engine using that database
  • exposes all Activiti services as Spring Beans
  • configures tidbits here and there such as the Activiti async job executor, mail server, etc.
    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    public class MyApp {

    public static void main(String[] args) {
    SpringApplication.run(MyApp.class, args);
    }
    }

BPMN process definitions

Set BPMN 2.0 process definition into the src/main/resources/processes folder. All processes placed here will automatically be deployed (ie. parsed and made to be executable) to the Activiti engine.

Step 1: create the process:
Let’s keep things simple to start, and create a CommandLineRunner that will be executed when the app boots up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
CommandLineRunner init( final RepositoryService repositoryService,
final RuntimeService runtimeService,
final TaskService taskService) {
return new CommandLineRunner() {
public void run(String... strings) throws Exception {
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("applicantName", "John Doe");
variables.put("email", "john.doe@activiti.com");
variables.put("phoneNumber", "123456789");
runtimeService.startProcessInstanceByKey("hireProcess", variables);
}
};
}

So what’s happening here is that we have created a map of all the variables needed to run the process and pass it when starting process. If you’d check the process definition you’ll see we reference those variables using ${variableName} in many places (e.g. the task description).

1
activiti:expression="${resumeService.storeResume()}"

Step 2: create the process bean
The process bean would be:

1
2
3
4
5
6
@Component
public class ResumeService {
public void storeResume() {
System.out.println("Storing resume ...");
}
}

So, when running the application, you’d see:

1
2
3
4
5
6
7
8
9
10
11
  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.2.0.RELEASE)

2015-02-16 11:55:11.129 INFO 304 --- [main] MyApp: Starting MyApp on The-Activiti-Machine.local with PID 304 ...
Storing resume ...
2015-02-16 11:55:13.662 INFO 304 --- [main] MyApp: Started MyApp in 2.788 seconds (JVM running for 3.067)

The Activiti REST API

  • Maven dependencies

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.activiti</groupId>
    <artifactId>spring-boot-starter-rest-api</artifactId>
    <version>${activiti.version}}</version>
    </dependency>
  • Configuration
    The REST API is secured by basic auth, and won’t have any users by default. So we will aadd an admin user to the system as shown below (add this to the MyApp class).
    ⚠️ Don’t do this in a production system of course!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Bean
    InitializingBean usersAndGroupsInitializer(final IdentityService identityService) {

    return new InitializingBean() {
    public void afterPropertiesSet() throws Exception {
    Group group = identityService.newGroup("user");
    group.setName("users");
    group.setType("security-role");
    identityService.saveGroup(group);

    User admin = identityService.newUser("admin");
    admin.setPassword("admin");
    identityService.saveUser(admin);
    }
    };
    }

    You can test this via curl command:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    curl -u admin:admin -H "Content-Type: application/json" -d '{"processDefinitionKey":"hireProcess", "variables": [ {"name":"applicantName", "value":"John Doe"}, {"name":"email", "value":"john.doe@alfresco.com"}, {"name":"phoneNumber",  "value":"1234567"} ]}' http://localhost:8080/runtime/process-instances
    {
    "tenantId": "",
    "url": "http://localhost:8080/runtime/process-instances/5",
    "activityId": "sid-42BAE58A-8FFB-4B02-AAED-E0D8EA5A7E39",
    "id": "5",
    "processDefinitionUrl": "http://localhost:8080/repository/process-definitions/hireProcess:1:4",
    "suspended": false,
    "completed": false,
    "ended": false,
    "businessKey": null,
    "variables": [],
    "processDefinitionId": "hireProcess:1:4"
    }

Actuator endpoint

  • Maven dependencies

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.activiti</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>${activiti.version}</version>
    </dependency>

    This adds a Spring Boot actuator endpoint for Activiti. If we restart the application, and hit http://localhost:8080/activiti/, we get some basic stats about our processes. With some imagination that in a live system you’ve got many more process definitions deployed and executing, you can see how this is useful.

    The same actuator is also registered as a JMX bean exposing similar information.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    completedTaskCountToday: 0,
    deployedProcessDefinitions: [
    "hireProcess (v1)"
    ],
    processDefinitionCount: 1,
    cachedProcessDefinitionCount: 1,
    runningProcessInstanceCount: {
    hireProcess (v1): 0
    },
    completedTaskCount: 0,
    completedActivities: 0,
    completedProcessInstanceCount: {
    hireProcess (v1): 0
    },
    openTaskCount: 0
    }

Dedicated REST endpoint

To finish our coding, we will create a dedicated REST endpoint for our hire process, that could be consumed by for example a javascript web application. So most likely, we’ll have a form for the applicant to fill in the details we’ve been passing programmatically above. And while we’re at it, let’s store the applicant information as a JPA entity. In that case, the data won’t be stored in Activiti anymore, but in a separate table and referenced by Activiti when needed.

1
2
3
4
5
<dependency>
<groupId>org.activiti</groupId>
<artifactId>spring-boot-starter-jpa</artifactId>
<version>${activiti.version}</version>
</dependency>

and add the entity to the MyApp class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
class Applicant {

@Id
@GeneratedValue
private Long id;

private String name;

private String email;

private String phoneNumber;

// Getters and setters

We’ll also need a Repository for this Entity (put this in a separate file or also in MyApp). No need for any methods, the Repository magic from Spring will generate the methods we need for us.

1
2
3
public interface ApplicantRepository extends JpaRepository<Applicant, Long> {
// ..
}

And now we can create the dedicated REST endpoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class MyRestController {

@Autowired
private RuntimeService runtimeService;

@Autowired
private ApplicantRepository applicantRepository;

@RequestMapping(value="/start-hire-process",
method= RequestMethod.POST, produces= MediaType.APPLICATION_JSON_VALUE)
public void startHireProcess(@RequestBody Map<String, String> data) {

Applicant applicant = new Applicant(data.get("name"),
data.get("email"), data.get("phoneNumber"));
applicantRepository.save(applicant);

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("applicant", applicant);
runtimeService.startProcessInstanceByKey("hireProcessWithJpa",
variables);
}
}

Let’s restart the application and start a new process instance:

1
curl -u admin:admin -H "Content-Type: application/json" -d '{"name":"John Doe", "email": "john.doe@alfresco.com",  "phoneNumber":"123456789"}' http://localhost:8080/start-hire-process

We can now go through our process. You could create a custom endpoints for this too, exposing different task queries with different forms.
Let’s see which task the process instance currently is at:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl -u admin:admin -H "Content-Type: application/json" http://localhost:8080/runtime/tasks
{
"order": "asc",
"size": 1,
"sort": "id",
"total": 1,
"data": [{
"id": "14",
"processInstanceId": "8",
"createTime": "2015-02-16T13:11:26.078+01:00",
"description": "Conduct a telephone interview with John Doe. Phone number = 123456789",
"name": "Telephone interview"
...
}],
"start": 0
}

So, our process is now at the Telephone interview. In a realistic application, there would be a task list and a form that could be filled in to complete this task. Let’s complete this task (we have to set the telephoneInterviewOutcome variable as the exclusive gateway uses it to route the execution):
(When we get the tasks again now, the process instance will have moved on to the two tasks in parallel in the subprocess (big rectangle):)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
curl -u admin:admin -H "Content-Type: application/json" -d '{"action" : "complete", "variables": [ {"name":"telephoneInterviewOutcome", "value":true} ]}' http://localhost:8080/runtime/tasks/14

{
"order": "asc",
"size": 2,
"sort": "id",
"total": 2,
"data": [
{
...
"name": "Tech interview"
},
{
...
"name": "Financial negotiation"
}
],
"start": 0
}

Testing

Junit + Springboot: test “happy path” (while omitting @Autowired fields and test e-mail server setup).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {MyApp.class})
@WebAppConfiguration
@IntegrationTest
public class HireProcessTest {

@Test
public void testHappyPath() {

// Create test applicant
Applicant applicant = new Applicant("John Doe", "john@activiti.org", "12344");
applicantRepository.save(applicant);

// Start process instance
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("applicant", applicant);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
"hireProcessWithJpa", variables);

// First, the 'phone interview' should be active
Task task = taskService.createTaskQuery()
.processInstanceId(processInstance.getId())
.taskCandidateGroup("dev-managers")
.singleResult();
Assert.assertEquals("Telephone interview", task.getName());

// Completing the phone interview with success should trigger two new tasks
Map<String, Object> taskVariables = new HashMap<String, Object>();
taskVariables.put("telephoneInterviewOutcome", true);
taskService.complete(task.getId(), taskVariables);

List<Task> tasks = taskService.createTaskQuery()
.processInstanceId(processInstance.getId())
.orderByTaskName().asc()
.list();
Assert.assertEquals(2, tasks.size());
Assert.assertEquals("Financial negotiation", tasks.get(0).getName());
Assert.assertEquals("Tech interview", tasks.get(1).getName());

// Completing both should wrap up the subprocess, send out the 'welcome mail'
// and end the process instance
taskVariables = new HashMap<String, Object>();
taskVariables.put("techOk", true);
taskService.complete(tasks.get(0).getId(), taskVariables);

taskVariables = new HashMap<String, Object>();
taskVariables.put("financialOk", true);
taskService.complete(tasks.get(1).getId(), taskVariables);

// Verify email
Assert.assertEquals(1, wiser.getMessages().size());

// Verify process completed
Assert.assertEquals(1, historyService.
createHistoricProcessInstanceQuery().finished().count());

}

Ten minutes example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class TenMinuteTutorial {
public static void main(String[] args) {
// Create Activiti process engine
ProcessEngine processEngine = ProcessEngineConfiguration
.createStandaloneProcessEngineConfiguration()
.buildProcessEngine();
// Get Activiti services
RepositoryService repositoryService = processEngine.getRepositoryService();
RuntimeService runtimeService = processEngine.getRuntimeService();
// Deploy the process definition
repositoryService.createDeployment()
.addClasspathResource("FinancialReportProcess.bpmn20.xml")
.deploy();
// Start a process instance
String procId = runtimeService.startProcessInstanceByKey("financialReport")
.getId();
// Get the first task
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery()
.taskCandidateGroup("accountancy").list();
for (Task task : tasks) {
System.out.println("Following task is available for accountancy group: "
+ task.getName());
// claim it
taskService.claim(task.getId(), "fozzie");
}
// Verify Fozzie can now retrieve the task
tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();
for (Task task : tasks) {
System.out.println("Task for fozzie: " + task.getName());
// Complete the task
taskService.complete(task.getId());
}
System.out.println("Number of tasks for fozzie: "
+ taskService.createTaskQuery().taskAssignee("fozzie").count());
// Retrieve and claim the second task
tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
for (Task task : tasks) {
System.out.println("Following task is available for management group: "
+ task.getName());
taskService.claim(task.getId(), "kermit");
}
// Completing the second task ends the process
for (Task task : tasks) {
taskService.complete(task.getId());
}
// verify that the process is actually finished
HistoryService historyService = processEngine.getHistoryService();
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery()
.processInstanceId(procId).singleResult();
System.out.println("Process instance end time: "
+ historicProcessInstance.getEndTime());
}
}
}


Further information:

BPM (Business Process Management)

  • Lifecycle stages:
    1. Design - by analyst and managers -> BPMN graph
    2. Model - developers and sysadmins -> add logic
    3. Execute - on a machine - get results
    4. Monitor - check the data from the machine
    5. Optimize - improve and go back to model
graph LR;
A(start);
B[order pizza];
C[bake pizza];
D[eat pizza];
E(finish)
A --> B;
B --> C;
C --> D;
D --> E;

Alfresco Activiti

Open source workflow in Java

  • engine
  • database
  • REST API
  • Spring framework (JPA, CDI, LDAP, JMX)

Process overview

All this can be called by REST

graph LR;
A[Model xml];
B[Process definition structure];
C[Process instance];
A --> |Deploy in Activiti| B
B --> |Triggers and user actionsi| C

Designing a model (xml)

Elements

  • Start events
  • Boundary events
  • Intermediate events
  • End events
  • Subprocess (normal line box)
  • Event subprocess (not continuous line box)
  • Call activiti (bold line box): may create new instance -> external call
  • Tasks (work units)
  • Gateways
  • Sequence flow

Events

  • Blank: API call
  • Timer (clock)
  • Message (postcardl) : local
  • *Signal (triangle):global
  • Cancel (cross)
  • Error (lightning): in error no case, not an exception, the right idea is to catch the exception and launch this event

Tasks

  • User task (you can specif the user role)
  • Service task (my Java code, call SOAP)
  • Script task (launch a script, js or groovy, internal logic)
  • There are also BusinessRuleTask, ReceiveTask, ManualTask, MailTask, CamelTask, MuleTask

Gateways

  • Exclusive (EX-OR, only one line on split, only waits for one)
  • Parallel (AND, several lines on split, waits for everyone to finish)
  • Inclusive (EX-AND, wait to continue)
  • Event-based - generates new process instance

Sequence flow

  • Normal
  • Default (crossed arrow)
  • Message flow (discontinuous)
  • Pools and lanes (e.g. vendor, client) -> to optimize

Best practices

  • Unique names
  • Avoid crossing flows
  • Modularize models
  • Naming conventions (verb + object)
  • Use comments
  • Avoid deadlocks and multimerges
  • Split flow with gateways
  • Avoid split and join at the same point
  • Avoid splitting tasks after events: get the result first
  • Void ecursive subprocess (beware infinite loops)
  • Consistent usage of Start and En events (only one start point)

❗ Note: an actor may have several roles (e.g. amazon has a picker and a packager in its storage place, but the same person can pick and package). Hence you can have a pool with 2 lanes.

Comunication with Activiti

  1. Go to activiti backend and extend (bad!)
  2. Use REST (good)

REST

  • Model
  • Deployments
  • Process definition
  • Process instances
  • Executions
  • Tasks
  • Froms
  • History
  • DB Tables
  • Jobs
  • Users and groups
    ⚠️ Add \service as a prefix to every REST call on the Activiti guide, and use camel case names for ids, so it’s directly mapped to Java.

The Activiti database

Tables:

  • ACT_AVT: events
  • ACT_GE_XXX: binaries, don’t touch
  • ACT_HI_XXX: history (read only, or that’s supposed. Don’t touch!!)
  • ACT_ID_XXX: users and groups
  • ACT_RU_XXX: runtime
    • IDENTITY_LINK: task X is assigned by user Y
    • EVENT_SUBSCR: event subscriber, listeners: task X will be done in 60 minutes
    • EXECUTION: be carefull with joins.
    • JOB: planned tasks (QuartzScheduler will execute a task) -> Quartz’s queue
    • TASK: they are timed
  • ACT_PROCDEF_INFO: the xml model parsed is stored here

⚠️ Don’t modify the schema! Upgrades can be horrible.
* Set associations in a different database.
* Be careful to be consistent.
* Activiti should be the master (don’t overwrite it with the associated DB data)

UIs

  • Activiti explorer
    It’s heavy on PROD. You only deploy this on demand. You usually connect to the database.

    • UI elements

      • Query
      • DB
      • Deployments: be carefull as it does “waterfall deletions”: delete a deployemnt and you’ll phisically destroy everything
    • Process

      • Instances (check active instances)
      • Process definitions (models) -> tenant: similar to environment, but you can only use it with REST
      • Model workspace : you can edit and deploy from here

    ⚠️ Tenants can only be modified via REST API by adding an input form and changing the value for “script”
    ⚠️ Use this app only for queries and very carefully, it’s easy to end in accidental cleansing fire.
    ⚠️ Activiti usually doens’t send back an answer after put (maybe 204). You should do a get after that.
    ⚠️ Error code 409 notifies conlicts (e.g. problems with concurrency and exclusion -> someone else already did that, so you can remove it from your cache)

  • Eclipse

  • Camunda

    • Big models

Properties

  • engine properties
    data
    1
    2
    3
    4
    # demo data
    cretate.demo.users = true
    cretate.demo.definitions = true
    cretate.demo.models report = true
  • db.properties: don’t forget about the connector
    1
    2
    3
    4
    5
    jdbc.type = mysql
    jdbc.driver = com.mysql.jdbc.Driver
    jdbc.url = jdbc:mysql://localhost:3306/activiti?autoReconnect=true
    username = root
    password =
  • log4j.properties
    1
    2
    3
    4
    5
    6
    log4j.rootLogger=INFO, CA

    # ConsoleAppender
    log4j.appender.CA=org.apache.log4j.ConsoleAppender
    log4j.appender.CA.layout=org.apache.log4j.PatternLayout
    log4j.appender.CA.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n

Methodology

3 options:

  • set up your_code inside Activiti -> set your code in the lib activiti in the war, mainly for REST

    • Fast: activiti provides
    • You set up your models in the jar or a external zip
    • Easier versioning
  • set up Activiti inside your_code -> create your own endpoints (horrible idea)

    • Modifier Activiti itself, and overwrites it (the horror!!)
    • you set it up “the hardcore way”, you are overwriting it
  • Use Activiti as a library -> reuse DB instances, your own models

    • Set is as a dependency
    • You extend it
  • Install on eclipse

    • Get 2 wars (activiti-explorer and activit-rest)
    • Copy them in a Tomcat (webapps folder)
    • Deploy tomcat so it creates the projects
    • Compile your project on eclipse (Activiti project, clean install)
    • Copy:
      • lib: copy jars in both projects
      • classes: copy properties in both projects
  • Implementation of the Java code

1
2
3
4
5
6
public class ClientRest implements JavaDelegate or extends BaseEntityEventListener{
...
public void execute(){
...
}
}

Introduction

Most of the work we do programming is automatizing tedious or repetive tasks. Then, the next obvious step would be a way to plan when should they start, or how to make them stop. For this task we use “Schedulers”.
We can use the basic Java TimerTasks, but that would be a huge effort as it is a very basic tool, and we may need something more advanced. One of the best advantages of using a framework is that we have tested scalable solutions which may be easily adapted to our need, hence, meet Quartz, a free open source library available under Apache 2.0 licence.
The idea is simple: we want to execute taks: which we will wrap in an extension of the class “Job”. Those Jobs will be handled in a main class which contains an Scheduler instance, which will check when it’s the right time to run them, and then it will execute those taks.

Process

1. Setting up the Maven dependencies

The first step is getting the testing dependencies into our project, and that’s something we can do via Maven by adding them to the pom.xml file.
❕These were the stable versions when the post was originally written

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>

2. The basic elements

2.1. The scheduler itself
The Schedulers instances are created by the library factories. The jobs can be scheduled anytime, but none will run unless we call the start method.

1
2
3
4
5
// Grab the Scheduler instance from the Factory
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

// and start it off
scheduler.start();

2.2. Job
The most basic execution unit. This will wrap thee repetitive task you’ll need to plan. The method execute is the most important part, as it is the “main” which will run.
Let’s see a useful example, we are going to prepare a task which will check what is already planned, and reload the time of execution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@DisallowConcurrentExecution
public class ReloaderJob implements Job {
public ReloaderJob() {
}

/**
* @see org.quartz.Job#execute(org.quartz.JobExecutionContext)
*/
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
SchedulerMain scheduler = (SchedulerMain) context.getJobDetail().getJobDataMap()
.getWrappedMap().get(AbstractJob.KEY_GET_RELOAD);
scheduler.reloadSchedulings();
}
}

2.3 JobDetail
The details to run them: what (Job class -> try to think of this as a facade for the real task) and when (Trigger).
If there are multiple parameters to pass, it may be good to use a DataMap to handle them:

1
2
3
4
5
6
7
8
9
10
11
public class TestJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getJobDetail().getJobDataMap();

String jobSays = dataMap.getString("jobSays");
float myValue = dataMap.getFloat("myValue");
System.out.println("Instance " + key + " of TestJob says: " + jobSays
+ ", and value is: " + myValue);
}
}

2.4. Basic triggers

And then schedule those jobs with triggers that define at what time(s) the job should run.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// define the job and tie it to our MyJob class
JobDetail job = newJob(MyJob.class)
.withIdentity("job1", "group1")
.build();
// Trigger the job to run now, and then repeat every 40 seconds
Trigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(40)
.repeatForever())
.build();
// Tell quartz to schedule the job using our trigger
scheduler.scheduleJob(job, trigger);

2.5. CronTrigger

The class which handles the syntax to specify when to run a planned job.
Its syntax basics are:

  • Seconds (0–59)
  • Minutes (0–59)
  • Hours (0–23)
  • Day-of-Month (1–31)
  • Month (JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC)
  • Day-of-Week (SUN, MON, TUE, WED, THU, FRI, SAT)
  • Year (optional field)

A few more complex examples:

  • 0 0/5 * * * ? → every 5 minutes
  • 10 0/5 * * * ? → every 5 minutes, at 10 seconds after the minute (10:00:10 am, 10:05:10 am, …)
  • 0 30 10–13 ? * WED,FRI → at 10:30, 11:30, 12:30, and 13:30, on every Wednesday and Friday

3. Basic architecture

The best way to understand how does this work is creating a quick example: 

3.1.- Scheduler prototype

Enumerator of the jobs to run: it has the full list of jobs, so every new job must be specified here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Defined jobs to display, containing name and the class to
* execute when triggered.
*/
public enum ScheduledJob {
TESTJOB("Test", org.anaklusmos.scheduler.jobs.TestJob.class),
RELOADERJOB("Reloader", org.anaklusmos.scheduler.jobs.Reloader.class);

private String name;
private Class<? extends Job> jobClass;

private ScheduledJob(String name, Class<? extends Job> jobClass) {
this.name = name;
this.jobClass = jobClass;
}

public String getName() {
return name;
}

public Class<? extends Job> getJobClass() {
return jobClass;
}
}
0%