rbpm 0.0.2 → 0.0.3

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.
@@ -0,0 +1,457 @@
1
+ #---------------------------------------------------------------------------------------------------------------------------------
2
+ #
3
+ # rbpm lightweight workflows, copyright (c) 2005 Christian Tschenett <furthermore@nospam@tschenett.ch>
4
+ #
5
+ # This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License
6
+ # as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
7
+ # This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
8
+ # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
9
+ # You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the
10
+ # Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
11
+ #
12
+ #---------------------------------------------------------------------------------------------------------------------------------
13
+
14
+ module Rbpm
15
+
16
+ #workflow base class. do not instantiate this class - always create sub classes!
17
+ #maybe you dont want to instantiate workflows directly or call workflow instance methods
18
+ #directly - use some sort of a workflow manager facade for this purpose.
19
+ class Workflow
20
+
21
+ ##WORKFLOW METADATA
22
+
23
+ #@@meta will be a class attribute of a concrete Workflow
24
+ def self.init_meta
25
+ module_eval <<-end_eval
26
+ unless defined? @@meta
27
+ @@meta = {}
28
+ def self.meta
29
+ @@meta
30
+ end
31
+ end
32
+ end_eval
33
+ end
34
+
35
+ #insert generic node helper...
36
+
37
+ #adds a node to the workflow.
38
+ def self.node(node_sym, options = {})
39
+ self.init_meta
40
+ self.nodes[node_sym] = {} unless self.nodes[node_sym]
41
+ options.each do |key,value|
42
+ case key.to_s
43
+ when /(.*)_trans$|trans$/
44
+ trans = $1.nil? ? "default" : $1
45
+ transition(node_sym, trans.to_sym, value)
46
+ when /(.*)_trans_action$|trans_action$/
47
+ trans = $1.nil? ? "default" : $1
48
+ transition_action(node_sym, trans.to_sym, value)
49
+ when /(.*)_trans_cond$|trans_cond$/
50
+ trans = $1.nil? ? "default" : $1
51
+ transition_condition(node_sym, trans.to_sym, value)
52
+ when "exception"
53
+ exception_transition(node_sym, value)
54
+ when "action"
55
+ node_action(node_sym, value)
56
+ when "on_enter"
57
+ node_enter_action(node_sym, value)
58
+ when "on_leave"
59
+ node_leave_action(node_sym, value)
60
+ when "action_singleton"
61
+ node_action_singleton(node_sym, value)
62
+ end
63
+ end
64
+ end
65
+
66
+ #insert special nodes helpers...
67
+
68
+ #adds the start node to the worklow. min/max one start node is allowed per workflow.
69
+ def self.start_node(node_sym, options = {})
70
+ node(node_sym, options)
71
+ nodes[node_sym][:start] = true
72
+ end
73
+
74
+ #adds an end node to the workflow.
75
+ def self.end_node(node_sym, options = {})
76
+ node(node_sym, options)
77
+ nodes[node_sym][:end] = true
78
+ end
79
+
80
+ #adds a state node to the workflow.
81
+ def self.state_node(node_sym, options = {})
82
+ node(node_sym, options)
83
+ nodes[node_sym][:state] = true
84
+ end
85
+
86
+ def self.create_join_node_instance
87
+ JoinNode.new
88
+ end
89
+
90
+ #adds a join node to the workflow.
91
+ def self.join_node(node_sym, options = {})
92
+ options[:action_singleton] = self.create_join_node_instance
93
+ node(node_sym, options)
94
+ end
95
+
96
+ def self.create_fork_node_instance(children_ctx)
97
+ ForkNode.new children_ctx
98
+ end
99
+
100
+ #adds a fork node to the workflow.
101
+ def self.fork_node(node_sym, options = {})
102
+ options[:action_singleton] = self.create_fork_node_instance(options[:children_ctx])
103
+ node(node_sym, options)
104
+ end
105
+
106
+ #adds a call node to the workflow.
107
+ def self.call_node(node_sym, options = {})
108
+ options[:action_singleton] = CallNode.new(options[:workflow], options[:workflow_version], options[:call_in], options[:call_out])
109
+ node(node_sym, options)
110
+ end
111
+
112
+ #insert node action helpers...
113
+
114
+ #adds a on_node_enter action block to the workflow.
115
+ def self.enter(node_sym, &blk)
116
+ node_enter_action(node_sym, blk)
117
+ end
118
+
119
+ def self.node_enter_action(node_sym, action)
120
+ nodes[node_sym][:on_enter] = action
121
+ end
122
+
123
+ def self.action(node_sym, &blk)
124
+ node_action(node_sym, blk)
125
+ end
126
+
127
+ def self.node_action(node_sym, action)
128
+ nodes[node_sym][:action] = action
129
+ end
130
+
131
+ #adds a on_node_leave action block to the workflow.
132
+ def self.leave(node_sym, &blk)
133
+ node_leave_action(node_sym, blk)
134
+ end
135
+
136
+ def self.node_leave_action(node_sym, action)
137
+ nodes[node_sym][:on_leave] = action
138
+ end
139
+
140
+ def self.node_action_singleton(node_sym, instance)
141
+ nodes[node_sym][:action_singleton] = instance
142
+ end
143
+
144
+ #insert transition, transition action and transition condition helpers...
145
+
146
+ def self.transition(node_from_sym, trans_sym, node_to_sym)
147
+ transitions(node_from_sym)[trans_sym] = node_to_sym
148
+ end
149
+
150
+ def self.transition_action(node_from_sym, trans_sym, action)
151
+ transition_actions(node_from_sym)[trans_sym] = action
152
+ end
153
+
154
+ #adds a on_transit action block to the workflow.
155
+ def self.transit(node_from_sym, trans_sym, &blk)
156
+ transition_action(node_from_sym, trans_sym, blk)
157
+ end
158
+
159
+ def self.transition_condition(node_from_sym, trans_sym, condition)
160
+ transition_conditions(node_from_sym)[trans_sym] = condition
161
+ end
162
+
163
+ def self.exception_transition(node_from_sym, node_to_sym)
164
+ transition(node_from_sym, :exception, node_to_sym)
165
+ transition_condition(node_from_sym, :exception, "false") #only executed if "hardcoded" signalled
166
+ end
167
+
168
+ #metadata access helpers...
169
+
170
+ def self.nodes
171
+ meta[:nodes] = {} unless meta[:nodes]
172
+ meta[:nodes]
173
+ end
174
+
175
+ def self.node_option_values(node, option)
176
+ nodes[node][option] = {} unless nodes[node][option]
177
+ nodes[node][option]
178
+ end
179
+
180
+ def self.transitions(node)
181
+ node_option_values(node, :transitions)
182
+ end
183
+
184
+ def self.transition_actions(node)
185
+ node_option_values(node, :transition_actions)
186
+ end
187
+
188
+ def self.transition_conditions(node)
189
+ node_option_values(node, :transition_conditions)
190
+ end
191
+
192
+ def self.find_first_node_with_option(option)
193
+ nodes.each do |node,options|
194
+ return node if options[option]
195
+ end
196
+ nil
197
+ end
198
+
199
+ def self.find_nodes_with_option(option)
200
+ found = nodes.collect do |node,options|
201
+ options[option] ? node : nil
202
+ end
203
+ found.compact
204
+ end
205
+
206
+ def self.find_node_with_name(name)
207
+ nodes[name.to_sym]
208
+ end
209
+
210
+ def self.find_start_node
211
+ find_first_node_with_option :start
212
+ end
213
+
214
+ def self.find_end_nodes
215
+ find_nodes_with_option :end
216
+ end
217
+
218
+ def self.end_node?(node)
219
+ nodes[node][:end]
220
+ end
221
+
222
+ @@next_token_id = 1
223
+
224
+ def self.generate_token_id
225
+ @@next_token_id += 1
226
+ end
227
+
228
+ ##WORKFLOW INSTANCE
229
+
230
+ attr_writer :token_id_generator
231
+ attr_reader :tokens, :result, :token_id_generator, :super_workflow_ref
232
+
233
+ def initialize(super_workflow_ref = -1)
234
+ @super_workflow_ref = super_workflow_ref
235
+ @tokens = []
236
+ @result = nil
237
+ end
238
+
239
+ def clazz
240
+ self.class
241
+ end
242
+
243
+ #token management...
244
+
245
+ def generate_token_id
246
+ if token_id_generator
247
+ token_id_generator.generate_token_id @super_workflow_ref
248
+ else
249
+ clazz.generate_token_id
250
+ end
251
+ end
252
+
253
+ def create_root_token(ctx_vars = {})
254
+ create_token(nil, ctx_vars)
255
+ end
256
+
257
+ def create_token_instance(parent = nil, ctx_vars = {})
258
+ Token.new(self, parent, ctx_vars, generate_token_id)
259
+ end
260
+
261
+ def create_token(parent = nil, ctx_vars = {})
262
+ token = create_token_instance(parent, ctx_vars)
263
+ @tokens << token
264
+ token
265
+ end
266
+
267
+ def token_done(token)
268
+ unless token.parent
269
+ @result = token.all_ctx_vars
270
+ end
271
+
272
+ @tokens.delete token
273
+ end
274
+
275
+ def find_root_token
276
+ @tokens.each do |token|
277
+ return token unless token.parent
278
+ end
279
+ nil?
280
+ end
281
+
282
+ def find_token(ref)
283
+ @tokens.each do |token|
284
+ found = token.find_token(ref)
285
+ if found
286
+ return found
287
+ end
288
+ end
289
+ nil?
290
+ end
291
+
292
+ def all_tokens(&blk)
293
+ @tokens.each do |token|
294
+ token.all_tokens(&blk)
295
+ end
296
+ end
297
+
298
+ def [](key)
299
+ if @result
300
+ @result[key]
301
+ else
302
+ find_root_token[key]
303
+ end
304
+ end
305
+
306
+ #(metadata-driven) reflection helpers...
307
+
308
+ def find_method(method_name)
309
+ action = method(method_name) if methods.include? method_name
310
+
311
+ unless action
312
+ action = clazz.method(method_name) if clazz.methods.include? method_name
313
+ end
314
+
315
+ action
316
+ end
317
+
318
+ def find_node_event_action(node, default_method_suffix, action_option)
319
+ default_method_name = "#{node.to_s}#{default_method_suffix}"
320
+
321
+ action_singleton = clazz.nodes[node][:action_singleton]
322
+ action = action_singleton.method(action_option) if action_singleton && action_singleton.methods.include?(action_option.to_s)
323
+
324
+ unless action
325
+ action = find_method(default_method_name)
326
+ end
327
+
328
+ unless action
329
+ action_or_name = clazz.nodes[node][action_option]
330
+
331
+ if action_or_name.is_a? Symbol
332
+ action_name = action_or_name.to_s
333
+ action = find_method(action_name)
334
+
335
+ raise "unknown custom action #{action_name}" unless action
336
+ else
337
+ action = action_or_name
338
+ end
339
+ end
340
+
341
+ action
342
+ end
343
+
344
+ def find_enter_action(node)
345
+ find_node_event_action(node, "_on_enter", :on_enter)
346
+ end
347
+
348
+ def find_leave_action(node)
349
+ find_node_event_action(node, "_on_leave", :on_leave)
350
+ end
351
+
352
+ def find_node_action(node)
353
+ find_node_event_action(node, "_action", :action)
354
+ end
355
+
356
+ def find_transit_action(node, transition)
357
+ default_method_name = "#{node.to_s}_#{transition.to_s}_trans_action"
358
+ action = find_method(default_method_name)
359
+
360
+ unless action
361
+ action_or_name = clazz.transition_actions(node)[transition]
362
+
363
+ if action_or_name.is_a? Symbol
364
+ action_name = action_or_name.to_s
365
+ action = find_method(action_name)
366
+
367
+ raise "unknown custom action #{action_name}" unless action
368
+ else
369
+ action = action_or_name
370
+ end
371
+ end
372
+
373
+ action
374
+ end
375
+
376
+ def find_transit_target(node, transition)
377
+ clazz.transitions(node)[transition]
378
+ end
379
+
380
+ #flow management...
381
+
382
+ #starts the workflow instance by creating the root token which
383
+ #takes a hidden transition to the start node. root token variable
384
+ #initialization is done as well.
385
+ #only call this method once per workflow instance!
386
+ def start(ctx_vars = {})
387
+ token = create_root_token(ctx_vars)
388
+ token.enter(clazz.find_start_node)
389
+ token
390
+ end
391
+
392
+ def call_event_action(action, token, node)
393
+ begin
394
+ action.call(token, node)
395
+ rescue Exception => exception
396
+ if clazz.transitions(node)[:exception]
397
+ token[:exception] = exception
398
+ return :exception
399
+ else
400
+ raise
401
+ end
402
+ end
403
+ return nil
404
+ end
405
+
406
+ def enter_node(token, node)
407
+ token.entered(node)
408
+
409
+ hardcoded_transition = call_event_action(find_enter_action(node), token, node) if find_enter_action(node)
410
+
411
+ if hardcoded_transition || (not (clazz.end_node?(node) || clazz.nodes[node][:state]))
412
+ signal_node(token, node, hardcoded_transition)
413
+ end
414
+ end
415
+
416
+ def signal_node(token, node, hardcoded_transition = nil)
417
+ if hardcoded_transition
418
+ transitions = [[hardcoded_transition, token]]
419
+ elsif find_node_action(node)
420
+ transitions = find_node_action(node).call(token, node)
421
+ else
422
+ clazz.transitions(node).each do |transition,target|
423
+ if (clazz.transition_conditions(node)[transition].nil? || token.eval_expr(clazz.transition_conditions(node)[transition]))
424
+ transitions = [[transition, token]]
425
+ break
426
+ end
427
+ end
428
+ end
429
+
430
+ if not clazz.nodes[node][:end] and transitions
431
+ take_transitions transitions
432
+ end
433
+ end
434
+
435
+ def take_transitions(transition_tokens)
436
+ #pseudo-parallel execution (token one leaves the node until it reaches a state. afterwards, token two leaves the node...)
437
+ transition_tokens.each do |transition_token|
438
+ transition,token = transition_token
439
+ token.transit(transition)
440
+ end
441
+ end
442
+
443
+ def leave_node(token, node)
444
+ call_event_action(find_leave_action(node), token, node) if find_leave_action(node)
445
+ end
446
+
447
+ def transit(token, node, transition)
448
+ hardcoded_transition = call_event_action(find_transit_action(node, transition), token, node) if find_transit_action(node, transition)
449
+
450
+ if hardcoded_transition
451
+ enter_node(token, find_transit_target(node, hardcoded_transition))
452
+ else
453
+ enter_node(token, find_transit_target(node, transition))
454
+ end
455
+ end
456
+ end
457
+ end