rbpm 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,18 @@
1
+ what is rBPM?
2
+
3
+ rbpm is a (JBoss-)jBPM-like lightweight workflow framework. it helps you to
4
+ develop asynchronous programs. developers describe their process definitions
5
+ (workflows) using extended ruby class definition syntax - no use of xml
6
+ files is planned.
7
+
8
+ license terms?
9
+
10
+ LGPL
11
+
12
+ author?
13
+
14
+ (c) 2005 by Christian Tschenett
15
+
16
+ getting startet...
17
+
18
+ take a look at the unit tests
Binary file
@@ -0,0 +1,12 @@
1
+ [ok] workflow instance persistence
2
+ [ok] states
3
+ [ok] context variables
4
+ [ok] versioning
5
+ [ok] more nodes (join, fork)
6
+ [ok] more nodes (call)
7
+ [ok] conditions
8
+ [~ok] consistent and clean meta syntax
9
+ [~ok] clean block support
10
+ [pending] rbpm server
11
+ [pending] rpbm server java/web service interface
12
+ [ok] workflow support for exception handling
@@ -0,0 +1,590 @@
1
+ class WorkflowVersionManager
2
+ def self.find_workflow_versions(workflow_name)
3
+ versions = []
4
+ ObjectSpace.each_object(Module) do |m|
5
+ module_name = m.to_s
6
+ if module_name =~ /#{workflow_name}Version([0-9]+)$/
7
+ versions << $1.to_i
8
+ end
9
+ end
10
+ versions
11
+ end
12
+
13
+ def self.create_workflow_instance(workflow_name, version = :latest)
14
+ workflow_name = workflow_name.to_s if workflow_name.is_a? Symbol
15
+ if :latest == version
16
+ versions = find_workflow_versions(workflow_name)
17
+ if versions.empty?
18
+ #fallback to non-versionned wf
19
+ return eval("#{workflow_name}.new")
20
+ else
21
+ version = versions.max
22
+ end
23
+ end
24
+ return eval("#{workflow_name}Version#{version}::#{workflow_name}.new")
25
+ end
26
+ end
27
+
28
+ class Workflow
29
+
30
+ ##WORKFLOW METADATA
31
+
32
+ #@@meta will be a class attribute of a concrete Workflow
33
+ def self.init_meta
34
+ module_eval <<-end_eval
35
+ unless defined? @@meta
36
+ @@meta = {}
37
+ def self.meta
38
+ @@meta
39
+ end
40
+ end
41
+ end_eval
42
+ end
43
+
44
+ #insert generic node helper...
45
+
46
+ def self.node(node_sym, options = {})
47
+ self.init_meta
48
+ self.nodes[node_sym] = {} unless self.nodes[node_sym]
49
+ options.each do |key,value|
50
+ case key.to_s
51
+ when /(.*)_trans$|trans$/
52
+ trans = $1.nil? ? "default" : $1
53
+ transition(node_sym, trans.to_sym, value)
54
+ when /(.*)_trans_action$|trans_action$/
55
+ trans = $1.nil? ? "default" : $1
56
+ transition_action(node_sym, trans.to_sym, value)
57
+ when /(.*)_trans_cond$|trans_cond$/
58
+ trans = $1.nil? ? "default" : $1
59
+ transition_condition(node_sym, trans.to_sym, value)
60
+ when "exception"
61
+ exception_transition(node_sym, value)
62
+ when "action"
63
+ node_action(node_sym, value)
64
+ when "on_enter"
65
+ node_enter_action(node_sym, value)
66
+ when "on_leave"
67
+ node_leave_action(node_sym, value)
68
+ when "action_singleton"
69
+ node_action_singleton(node_sym, value)
70
+ end
71
+ end
72
+ end
73
+
74
+ #insert special nodes helpers...
75
+
76
+ def self.start_node(node_sym, options = {})
77
+ node(node_sym, options)
78
+ nodes[node_sym][:start] = true
79
+ end
80
+
81
+ def self.end_node(node_sym, options = {})
82
+ node(node_sym, options)
83
+ nodes[node_sym][:end] = true
84
+ end
85
+
86
+ def self.state_node(node_sym, options = {})
87
+ node(node_sym, options)
88
+ nodes[node_sym][:state] = true
89
+ end
90
+
91
+ def self.join_node(node_sym, options = {})
92
+ options[:action_singleton] = JoinNode.new
93
+ node(node_sym, options)
94
+ end
95
+
96
+ def self.fork_node(node_sym, options = {})
97
+ options[:action_singleton] = ForkNode.new(options[:children_ctx])
98
+ node(node_sym, options)
99
+ end
100
+
101
+ def self.call_node(node_sym, options = {})
102
+ options[:action_singleton] = CallNode.new(options[:workflow], options[:workflow_version], options[:call_in], options[:call_out])
103
+ node(node_sym, options)
104
+ end
105
+
106
+ #insert node action helpers...
107
+
108
+ def self.enter(node_sym, &blk)
109
+ node_enter_action(node_sym, blk)
110
+ end
111
+
112
+ def self.node_enter_action(node_sym, action)
113
+ nodes[node_sym][:on_enter] = action
114
+ end
115
+
116
+ def self.action(node_sym, &blk)
117
+ node_action(node_sym, blk)
118
+ end
119
+
120
+ def self.node_action(node_sym, action)
121
+ nodes[node_sym][:action] = action
122
+ end
123
+
124
+ def self.leave(node_sym, &blk)
125
+ node_leave_action(node_sym, blk)
126
+ end
127
+
128
+ def self.node_leave_action(node_sym, action)
129
+ nodes[node_sym][:on_leave] = action
130
+ end
131
+
132
+ def self.node_action_singleton(node_sym, instance)
133
+ nodes[node_sym][:action_singleton] = instance
134
+ end
135
+
136
+ #insert transition, transition action and transition condition helpers...
137
+
138
+ def self.transition(node_from_sym, trans_sym, node_to_sym)
139
+ transitions(node_from_sym)[trans_sym] = node_to_sym
140
+ end
141
+
142
+ def self.transition_action(node_from_sym, trans_sym, action)
143
+ transition_actions(node_from_sym)[trans_sym] = action
144
+ end
145
+
146
+ def self.transition_condition(node_from_sym, trans_sym, condition)
147
+ transition_conditions(node_from_sym)[trans_sym] = condition
148
+ end
149
+
150
+ def self.exception_transition(node_from_sym, node_to_sym)
151
+ transition(node_from_sym, :exception, node_to_sym)
152
+ transition_condition(node_from_sym, :exception, "false") #only executed if "hardcoded" signalled
153
+ end
154
+
155
+ #metadata access helpers...
156
+
157
+ def self.nodes
158
+ meta[:nodes] = {} unless meta[:nodes]
159
+ meta[:nodes]
160
+ end
161
+
162
+ def self.node_option_values(node, option)
163
+ nodes[node][option] = {} unless nodes[node][option]
164
+ nodes[node][option]
165
+ end
166
+
167
+ def self.transitions(node)
168
+ node_option_values(node, :transitions)
169
+ end
170
+
171
+ def self.transition_actions(node)
172
+ node_option_values(node, :transition_actions)
173
+ end
174
+
175
+ def self.transition_conditions(node)
176
+ node_option_values(node, :transition_conditions)
177
+ end
178
+
179
+ def self.find_first_node_with_option(option)
180
+ nodes.each do |node,options|
181
+ return node if options[option]
182
+ end
183
+ nil
184
+ end
185
+
186
+ def self.find_nodes_with_option(option)
187
+ found = nodes.collect do |node,options|
188
+ options[option] ? node : nil
189
+ end
190
+ found.compact
191
+ end
192
+
193
+ def self.find_node_with_name(name)
194
+ nodes[name.to_sym]
195
+ end
196
+
197
+ def self.find_start_node
198
+ find_first_node_with_option :start
199
+ end
200
+
201
+ def self.find_end_nodes
202
+ find_nodes_with_option :end
203
+ end
204
+
205
+ def self.end_node?(node)
206
+ nodes[node][:end]
207
+ end
208
+
209
+ @@next_token_id = 1
210
+
211
+ def self.generate_token_id
212
+ @@next_token_id += 1
213
+ end
214
+
215
+ ##WORKFLOW INSTANCE
216
+
217
+ attr_writer :token_id_generator
218
+ attr_reader :tokens, :result, :token_id_generator
219
+
220
+ def initialize
221
+ @tokens = []
222
+ @result = nil
223
+ end
224
+
225
+ def clazz
226
+ self.class
227
+ end
228
+
229
+ #token management...
230
+
231
+ def generate_token_id
232
+ if token_id_generator
233
+ token_id_generator.generate_token_id
234
+ else
235
+ clazz.generate_token_id
236
+ end
237
+ end
238
+
239
+ def create_root_token(ctx_vars = {})
240
+ create_token(nil, ctx_vars)
241
+ end
242
+
243
+ def create_token(parent = nil, ctx_vars = {})
244
+ token = Token.new(self, parent, ctx_vars, generate_token_id)
245
+ @tokens << token
246
+ token
247
+ end
248
+
249
+ def token_done(token)
250
+ unless token.parent
251
+ @result = token.all_ctx_vars
252
+ end
253
+
254
+ @tokens.delete token
255
+ end
256
+
257
+ def find_root_token
258
+ @tokens.each do |token|
259
+ return token unless token.parent
260
+ end
261
+ nil?
262
+ end
263
+
264
+ def [](key)
265
+ if @result
266
+ @result[key]
267
+ else
268
+ find_root_token[key]
269
+ end
270
+ end
271
+
272
+ #(metadata-driven) reflection helpers...
273
+
274
+ def find_method(method_name)
275
+ action = method(method_name) if methods.include? method_name
276
+
277
+ unless action
278
+ action = clazz.method(method_name) if clazz.methods.include? method_name
279
+ end
280
+
281
+ action
282
+ end
283
+
284
+ def find_node_event_action(node, default_method_suffix, action_option)
285
+ default_method_name = "#{node.to_s}#{default_method_suffix}"
286
+
287
+ action_singleton = clazz.nodes[node][:action_singleton]
288
+ action = action_singleton.method(action_option) if action_singleton && action_singleton.methods.include?(action_option.to_s)
289
+
290
+ unless action
291
+ action = find_method(default_method_name)
292
+ end
293
+
294
+ unless action
295
+ action_or_name = clazz.nodes[node][action_option]
296
+
297
+ if action_or_name.is_a? Symbol
298
+ action_name = action_or_name.to_s
299
+ action = find_method(action_name)
300
+
301
+ raise "unknown custom action #{action_name}" unless action
302
+ else
303
+ action = action_or_name
304
+ end
305
+ end
306
+
307
+ action
308
+ end
309
+
310
+ def find_enter_action(node)
311
+ find_node_event_action(node, "_on_enter", :on_enter)
312
+ end
313
+
314
+ def find_leave_action(node)
315
+ find_node_event_action(node, "_on_leave", :on_leave)
316
+ end
317
+
318
+ def find_node_action(node)
319
+ find_node_event_action(node, "_action", :action)
320
+ end
321
+
322
+ def find_transit_action(node, transition)
323
+ default_method_name = "#{node.to_s}_#{transition.to_s}_trans_action"
324
+ action = find_method(default_method_name)
325
+
326
+ unless action
327
+ action_or_name = clazz.transition_actions(node)[transition]
328
+
329
+ if action_or_name.is_a? Symbol
330
+ action_name = action_or_name.to_s
331
+ action = find_method(action_name)
332
+
333
+ raise "unknown custom action #{action_name}" unless action
334
+ else
335
+ action = action_or_name
336
+ end
337
+ end
338
+
339
+ action
340
+ end
341
+
342
+ def find_transit_target(node, transition)
343
+ clazz.transitions(node)[transition]
344
+ end
345
+
346
+ #flow management...
347
+
348
+ def start(ctx_vars = {})
349
+ token = create_root_token(ctx_vars)
350
+ token.enter(clazz.find_start_node)
351
+ token
352
+ end
353
+
354
+ def call_event_action(action, token, node)
355
+ begin
356
+ action.call(token, node)
357
+ rescue Exception => exception
358
+ if clazz.transitions(node)[:exception]
359
+ token[:exception] = exception
360
+ return :exception
361
+ else
362
+ raise
363
+ end
364
+ end
365
+ return nil
366
+ end
367
+
368
+ def enter_node(token, node)
369
+ token.entered(node)
370
+
371
+ hardcoded_transition = call_event_action(find_enter_action(node), token, node) if find_enter_action(node)
372
+
373
+ if hardcoded_transition || (not (clazz.end_node?(node) || clazz.nodes[node][:state]))
374
+ signal_node(token, node, hardcoded_transition)
375
+ end
376
+ end
377
+
378
+ def signal_node(token, node, hardcoded_transition = nil)
379
+ if hardcoded_transition
380
+ transitions = [[hardcoded_transition, token]]
381
+ elsif find_node_action(node)
382
+ transitions = find_node_action(node).call(token, node)
383
+ else
384
+ clazz.transitions(node).each do |transition,target|
385
+ if (clazz.transition_conditions(node)[transition].nil? || token.eval_expr(clazz.transition_conditions(node)[transition]))
386
+ transitions = [[transition, token]]
387
+ break
388
+ end
389
+ end
390
+ end
391
+
392
+ if not clazz.nodes[node][:end] and transitions
393
+ transitions.each do |transition_token|
394
+ transition,token = transition_token
395
+ token.transit(transition)
396
+ end
397
+ end
398
+ end
399
+
400
+ def leave_node(token, node)
401
+ call_event_action(find_leave_action(node), token, node) if find_leave_action(node)
402
+ end
403
+
404
+ def transit(token, node, transition)
405
+ hardcoded_transition = call_event_action(find_transit_action(node, transition), token, node) if find_transit_action(node, transition)
406
+
407
+ if hardcoded_transition
408
+ enter_node(token, find_transit_target(node, hardcoded_transition))
409
+ else
410
+ enter_node(token, find_transit_target(node, transition))
411
+ end
412
+ end
413
+ end
414
+
415
+ class Token
416
+
417
+ attr_writer :sub_process, :parent_process
418
+ attr_reader :workflow, :node, :parent, :sub_process, :parent_process, :ref
419
+
420
+ def initialize(workflow, parent = nil, ctx_vars = {}, ref = -1)
421
+ @workflow = workflow
422
+ @parent = parent
423
+ @ctx_vars = ctx_vars
424
+ @ref = ref
425
+ @childs = []
426
+ @sub_process = nil
427
+ @parent_process = nil
428
+ @done = false
429
+ if parent
430
+ @node = parent.node
431
+ @parent.add_child(self)
432
+ else
433
+ @node = nil
434
+ end
435
+ end
436
+
437
+ def all_ctx_vars
438
+ @ctx_vars
439
+ end
440
+
441
+ def token
442
+ self
443
+ end
444
+
445
+ def []=(key, value)
446
+ @ctx_vars[key] = value
447
+ end
448
+
449
+ def [](key)
450
+ @ctx_vars[key]
451
+ end
452
+
453
+ def add_child(token)
454
+ @childs << token
455
+ end
456
+
457
+ def remove_child(token)
458
+ @childs.delete(token)
459
+ end
460
+
461
+ def childs?
462
+ not @childs.empty?
463
+ end
464
+
465
+ def signal
466
+ @workflow.signal_node(self, node)
467
+
468
+ if end? && @parent_process
469
+ @parent_process.signal
470
+ @parent_process = nil
471
+ end
472
+ end
473
+
474
+ def enter(node)
475
+ @workflow.enter_node(self, node)
476
+ end
477
+
478
+ def entered(node)
479
+ @node = node
480
+
481
+ if @workflow.clazz.end_node?(@node) and not @parent
482
+ @workflow.token_done(self)
483
+ end
484
+ end
485
+
486
+ def transit(transition)
487
+ hardcoded_transition = @workflow.leave_node(self, @node)
488
+ if hardcoded_transition
489
+ @workflow.transit(self, @node, hardcoded_transition)
490
+ else
491
+ @workflow.transit(self, @node, transition)
492
+ end
493
+ end
494
+
495
+ def jump(child)
496
+ @node = child.node
497
+ end
498
+
499
+ def done
500
+ @parent.remove_child(self) if @parent
501
+ @parent.jump(self) if @parent
502
+ @workflow.token_done(self)
503
+ @done = true
504
+ end
505
+
506
+ def end?
507
+ @done || @workflow.clazz.end_node?(@node)
508
+ end
509
+
510
+ def fork(ctx_vars = {})
511
+ @workflow.create_token(self, ctx_vars)
512
+ end
513
+
514
+ def eval_expr(expr)
515
+ eval(expr, binding())
516
+ end
517
+ end
518
+
519
+ class ForkNode
520
+ def initialize(children_ctx_var)
521
+ @children_ctx_var = children_ctx_var
522
+ end
523
+
524
+ def action(token, caller)
525
+ exits = []
526
+ token[@children_ctx_var].each do |ctx_vars|
527
+ exits << [:default, token.fork(ctx_vars)]
528
+ end
529
+ return exits
530
+ end
531
+ end
532
+
533
+ class JoinNode
534
+ def action(token, caller)
535
+ parent_token = token.parent
536
+ token.done
537
+ parent_token.childs? ? nil : [[:default, parent_token]]
538
+ end
539
+ end
540
+
541
+ class CallNode
542
+ def initialize(workflow, workflow_version, call_in, call_out)
543
+ @workflow = workflow
544
+ @workflow_version = workflow_version
545
+ @call_in = call_in
546
+ @call_out = call_out
547
+ end
548
+
549
+ def action(token, caller)
550
+ if token.sub_process
551
+ #sub process has been finished...
552
+
553
+ @call_out.each do |remote,local|
554
+ token[local] = token.sub_process[remote]
555
+ end
556
+
557
+ token.sub_process = nil
558
+
559
+ return [[:default, token]]
560
+ else
561
+ #let's start a sub process...
562
+
563
+ if (@workflow.is_a? Symbol)
564
+ sub_wf = WorkflowVersionManager.create_workflow_instance(@workflow, @workflow_version.nil? ? :latest : @workflow_version)
565
+ else
566
+ sub_wf = @workflow.new
567
+ end
568
+
569
+ params = {}
570
+ @call_in.each do |local,remote|
571
+ params[remote] = token[local]
572
+ end
573
+
574
+ sub_t = sub_wf.start(params)
575
+
576
+ if (sub_t.end?)
577
+ @call_out.each do |remote,local|
578
+ token[local] = sub_t[remote]
579
+ end
580
+
581
+ return [[:default, token]]
582
+ else
583
+ sub_t.parent_process = token
584
+ token.sub_process = sub_t
585
+
586
+ return nil #stop wf execution... we'll be called later...
587
+ end
588
+ end
589
+ end
590
+ end