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,109 @@
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
+ #the arriving (parent-) token won't leave the node using the node's default transition. in lieu thereof, a number
17
+ #of cild tokens will be generated and every child token will leave the node using the node's :default transition
18
+ #immediately. only one leaving transition - :default - is allowed. the number of child tokens is determined by
19
+ #the length of the :children_ctx token context variable. the :children_ctx token variable holds a hash of initial
20
+ #context variables for every child token (:token_variable_name => token_variable_content hash).
21
+ class ForkNode
22
+ def initialize(children_ctx_var)
23
+ @children_ctx_var = children_ctx_var
24
+ end
25
+
26
+ def action(token, caller)
27
+ exits = []
28
+ token[@children_ctx_var].each do |ctx_vars|
29
+ exits << [:default, token.fork(ctx_vars)]
30
+ end
31
+ return exits
32
+ end
33
+ end
34
+
35
+ #arriving child tokens - generated by a fork node - are consumed upon arrival. the last arriving child token transfers the
36
+ #child's parent token to this node which will leave the node immediately using the node's :default transition. thus, only
37
+ #one leaving transition - :default - is allowed. children token variables will also be lost upon arrival of their associated
38
+ #tokens - you have to save child tokens variables manually if you want to preserve them (by copying them to the parent token's
39
+ #context, for instance)
40
+ class JoinNode
41
+ def action(token, caller)
42
+ parent_token = token.parent
43
+ token.done
44
+ parent_token.childs? ? nil : [[:default, parent_token]]
45
+ end
46
+ end
47
+
48
+ #the arriving token triggers the creation and execution of a named sub process instance/workflow. token variables mentioned in
49
+ #the :call_in mapping are copied from arriving token's context to the root token of the newly created sub process instance
50
+ #before starting the sub process instance. if the sub workflow does not contain states - and therefore the sub
51
+ #workflow will terminate immediately -, the token will leave the call node
52
+ #immediately using the node's :default transition. only one leaving transition - :default - is allowed. before leaving,
53
+ #all token variables mentioned in the :call_out mapping are copied from the sub workflow's root token context to the
54
+ #context of the token waiting in the call node. otherwise, if the sub workflow's root token arrived in a state, the token remains in the
55
+ #call node and - from a workflow user's point of view - the call node behaves like a state node. but do not call signal
56
+ #on the token waiting in the call node! you have to call signal on the sub workflow's token waiting in the sub workflow's
57
+ #state node - the node waiting in the call node will be signaled automatically upon sub workflow termination.
58
+ class CallNode
59
+ def initialize(workflow, workflow_version, call_in, call_out)
60
+ @workflow = workflow
61
+ @workflow_version = workflow_version
62
+ @call_in = call_in
63
+ @call_out = call_out
64
+ end
65
+
66
+ def action(token, caller)
67
+ if token.sub_process
68
+ #sub process has been finished...
69
+
70
+ @call_out.each do |remote,local|
71
+ token[local] = token.sub_process[remote]
72
+ end
73
+
74
+ token.sub_process = nil
75
+
76
+ return [[:default, token]]
77
+ else
78
+ #let's start a sub process...
79
+
80
+ if (@workflow.is_a? Symbol)
81
+ sub_wf = WorkflowVersionManager.create_workflow_instance(@workflow, @workflow_version.nil? ? :latest : @workflow_version, token.workflow.super_workflow_ref)
82
+ else
83
+ sub_wf = @workflow.new(token.workflow.super_workflow_ref)
84
+ end
85
+ sub_wf.token_id_generator = token.workflow.token_id_generator
86
+
87
+ params = {}
88
+ @call_in.each do |local,remote|
89
+ params[remote] = token[local]
90
+ end
91
+
92
+ sub_t = sub_wf.start(params)
93
+
94
+ if (sub_t.end?)
95
+ @call_out.each do |remote,local|
96
+ token[local] = sub_t[remote]
97
+ end
98
+
99
+ return [[:default, token]]
100
+ else
101
+ sub_t.parent_process = token
102
+ token.sub_process = sub_t
103
+
104
+ return nil #stop wf execution... we'll be called later...
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,265 @@
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
+ require 'yaml'
15
+
16
+ module Rbpm
17
+ #placeholder
18
+ end
19
+
20
+ module Rbpm::FlockFileSystem
21
+
22
+ class WorkflowPersistenceManager
23
+ def initialize(index_file_path)
24
+ @token_list = WorkflowTokenIndex.new index_file_path
25
+ end
26
+
27
+ #creates a - named - workflow and starts the workflow afterwards.
28
+ #if the workflow is not finished - a token waits in a state -
29
+ #after execution, the current state of the workflow - and
30
+ #the state of started sub workflows as well - will be
31
+ #persisted in a YAML file associated with the workflow instance.
32
+ #furthermore, all active tokens - and child tokens, and
33
+ #tokens of started sub processes as well - are added to the
34
+ #token list and the current state of the token list (some sort
35
+ #of index which allows to find a persisted workflow associated
36
+ #with a token) will be persisted too.
37
+ def create_and_start(workflow_name, token_vars = {})
38
+ wf_ref = @token_list.create_new_super_workflow_id
39
+ begin
40
+ wf = Rbpm::WorkflowVersionManager.create_workflow_instance workflow_name, :latest, wf_ref
41
+ wf.token_id_generator = @token_list
42
+ t = wf.start token_vars
43
+ unless t.end?
44
+ create_persistent_workflow wf, wf_ref
45
+ wf_ref = nil
46
+ end
47
+ return t.freeze
48
+ ensure
49
+ @token_list.update_workflow_tokens(wf_ref, []) if wf_ref
50
+ end
51
+ end
52
+
53
+ #sends a signal to a token associated with a persisted workflow (only token
54
+ #residing in a state should be signaled). the state of the "root-parent" workflow
55
+ #of the workflow associated with the token - can be the workflow itself :) -
56
+ #is loaded from disk - and therefore the state of all sub workflows is restored as well -
57
+ #and the token is signalled afterwards. from now on, this method behaves like
58
+ #create_and_start: the state is only persisted again, if there is at least one
59
+ #token waiting in a state after execution. this means that you business logic
60
+ #has to preserve all relevant information stored as token context variables!
61
+ def signal(token_ref, token_vars = {})
62
+ wf_ref = @token_list.lookup_super_workflow token_ref
63
+ wf_tokens = []
64
+ begin
65
+ modify_lock_workflow(wf_ref, true) do |wf|
66
+ token = wf.find_token token_ref
67
+ token_vars.each do |key,value|
68
+ token[key] = value
69
+ end
70
+ token.signal
71
+ wf.all_tokens do |wf_token|
72
+ wf_tokens << wf_token.ref unless wf_token.end?
73
+ end
74
+ return token.freeze
75
+ end
76
+ ensure
77
+ @token_list.update_workflow_tokens wf_ref, wf_tokens
78
+ end
79
+ end
80
+
81
+ #returns a freezed version of a token associated with a persisted workflow. the
82
+ #workflow is loaded using the logic mentioned in the comment of the signal method.
83
+ #using this method you can gain access to token variables of tokens waiting
84
+ #in states. as mentioned above, you cannot access the token context of "finished"
85
+ #tokens because they are already destroyed!
86
+ def freezed_token(token_ref)
87
+ wf_ref = @token_list.lookup_super_workflow token_ref
88
+ modify_lock_workflow(wf_ref, false) do |wf|
89
+ token = wf.find_token token_ref
90
+ return token.freeze
91
+ end
92
+ end
93
+
94
+ protected
95
+
96
+ def create_persistent_workflow(wf, wf_ref)
97
+ file = File.open("rbpm-workflow-#{wf_ref}.yml", "w+") do |file|
98
+ file.flock(File::LOCK_EX)
99
+ write(file, wf)
100
+ file.flock(File::LOCK_UN)
101
+ end
102
+ wf_tokens = []
103
+ wf.all_tokens do |wf_token|
104
+ wf_tokens << wf_token.ref unless wf_token.end?
105
+ end
106
+ @token_list.update_workflow_tokens wf_ref, wf_tokens
107
+ end
108
+
109
+ def modify_lock_workflow(wf_ref, write_back) #do |wf|
110
+ delete_path = nil
111
+ begin
112
+ file = File.open("rbpm-workflow-#{wf_ref}.yml", "r+") do |file|
113
+ file.flock(File::LOCK_EX)
114
+ begin
115
+ wf = read(file)
116
+ begin
117
+ yield(wf)
118
+ ensure
119
+ unless wf.find_root_token
120
+ delete_path = file.path
121
+ else
122
+ if write_back
123
+ file.pos = 0
124
+ file.truncate 0
125
+ write(file, wf)
126
+ end
127
+ end
128
+ end
129
+ ensure
130
+ file.flock(File::LOCK_UN)
131
+ end
132
+ end
133
+ ensure
134
+ File.delete delete_path if delete_path
135
+ end
136
+ end
137
+
138
+ def read(f)
139
+ content = ""
140
+ while (line = f.gets)
141
+ content = content + "\n" + line
142
+ end
143
+ YAML.load(content)
144
+ end
145
+
146
+ def write(file, object)
147
+ content = YAML.dump(object)
148
+ file.puts content
149
+ end
150
+ end
151
+
152
+ class WorkflowTokenIndex
153
+ def initialize(index_file_path)
154
+ @index_file_path = index_file_path
155
+ end
156
+
157
+ def lookup_super_workflow(lookup_token_id)
158
+ return nil unless index_file_exists?
159
+ file = open_read_locked_index_file
160
+ begin
161
+ while (token_workflow = read_index_entry file)
162
+ token_id, workflow_id = token_workflow
163
+ return workflow_id if token_id == lookup_token_id
164
+ end
165
+ ensure
166
+ close_unlock_file file
167
+ end
168
+ return nil
169
+ end
170
+
171
+ def update_workflow_tokens(super_workflow_id, token_ids)
172
+ manage_workflow_tokens :update_workflow_tokens, super_workflow_id, token_ids
173
+ end
174
+
175
+ def create_new_super_workflow_id
176
+ manage_workflow_tokens
177
+ end
178
+
179
+ def generate_token_id(super_workflow_id)
180
+ manage_workflow_tokens :create_new_workflow_token_id, super_workflow_id
181
+ end
182
+
183
+ protected
184
+
185
+ def manage_workflow_tokens(operation = :create_super_workflow_and_placeholder_token, super_workflow_id = nil, token_ids = [])
186
+ token_workflows = []
187
+ not_empty = index_file_exists?
188
+ file = not_empty ? open_readwrite_locked_index_file : open_create_locked_index_file
189
+ begin
190
+ max_id = -1;
191
+ if not_empty
192
+ while (token_workflow = read_index_entry file)
193
+ token_id, workflow_id = token_workflow
194
+ case operation
195
+ when :update_workflow_tokens
196
+ token_workflows << token_workflow unless workflow_id == super_workflow_id
197
+ when :create_super_workflow_and_placeholder_token
198
+ token_workflows << token_workflow
199
+ max_id = workflow_id if workflow_id > max_id
200
+ when :create_new_workflow_token_id
201
+ token_workflows << token_workflow
202
+ max_id = token_id if token_id > max_id
203
+ end
204
+ end
205
+ file.pos = 0
206
+ file.truncate 0
207
+ end
208
+ case operation
209
+ when :create_super_workflow_and_placeholder_token
210
+ super_workflow_id = (max_id + 1)
211
+ token_ids << -1
212
+ when :create_new_workflow_token_id
213
+ token_ids << (max_id + 1)
214
+ end
215
+ token_ids.each do |token_id|
216
+ token_workflows << [token_id, super_workflow_id]
217
+ end
218
+ token_workflows.each do |token_workflow|
219
+ token_id, workflow_id = token_workflow
220
+ file.puts token_id
221
+ file.puts workflow_id
222
+ end
223
+ return (max_id + 1)
224
+ ensure
225
+ close_unlock_file file
226
+ File.delete file.path if token_workflows.empty?
227
+ end
228
+ end
229
+
230
+ def index_file_exists?
231
+ File.exist?(@index_file_path)
232
+ end
233
+
234
+ def open_read_locked_index_file
235
+ file = File.open(@index_file_path, "r")
236
+ file.flock(File::LOCK_EX)
237
+ return file
238
+ end
239
+
240
+ def open_readwrite_locked_index_file
241
+ file = File.open(@index_file_path, "r+")
242
+ file.flock(File::LOCK_EX)
243
+ return file
244
+ end
245
+
246
+ def open_create_locked_index_file
247
+ file = File.open(@index_file_path, "w+")
248
+ file.flock(File::LOCK_EX)
249
+ return file
250
+ end
251
+
252
+ def read_index_entry(file)
253
+ token_id = file.gets
254
+ return nil unless
255
+ workflow_id = file.gets
256
+ return nil unless workflow_id
257
+ return [token_id.to_i, workflow_id.to_i]
258
+ end
259
+
260
+ def close_unlock_file(file)
261
+ file.flock(File::LOCK_UN)
262
+ file.close
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,46 @@
1
+ begin
2
+ require 'rubygems'
3
+ require_gem 'rbpm', '>= 0.0.3'
4
+ rescue LoadError
5
+ require '../lib/rbpm'
6
+ end
7
+
8
+ def args_to_hash(args, start_index = 1)
9
+ result = {}
10
+ if args.length > start_index
11
+ key = nil
12
+ args[start_index .. args.length - 1].each do |arg|
13
+ if key
14
+ result[key.to_sym] = arg
15
+ key = nil
16
+ else
17
+ key = arg
18
+ end
19
+ end
20
+ end
21
+ result
22
+ end
23
+
24
+ #note: don't forget to load (-r) your workflow. sample: ruby -r sample_wf generic_persistent_workflow_manager.rb create "SampleWorkflow"
25
+
26
+ mgr = Rbpm::FlockFileSystem::WorkflowPersistenceManager.new "ptoken.ind" #stores workflows in the current working directory!
27
+ cmd = ARGV[0].to_sym
28
+
29
+ case cmd
30
+ when :create
31
+ workflow_name = ARGV[1]
32
+ token_vars = args_to_hash ARGV, 2
33
+ token = mgr.create_and_start(workflow_name, token_vars)
34
+ puts token.ref
35
+ exit(token.end? ? 0 : token.ref)
36
+ when :signal
37
+ token_ref = ARGV[1].to_i
38
+ token_vars = args_to_hash ARGV, 2
39
+ token = mgr.signal(token_ref, token_vars)
40
+ exit(token.end? ? 0 : token.ref)
41
+ when :print
42
+ token_ref = ARGV[1].to_i
43
+ ctx_var = ARGV[2].to_sym
44
+ token = mgr.freezed_token(token_ref)
45
+ puts token[ctx_var]
46
+ end
@@ -0,0 +1,169 @@
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
+ #tokens traverse a directed graph composed of nodes and transitions. they cause execution
17
+ #of event actions upon arrival in a node, before leaving a node or while moving from one
18
+ #node to another by taking a transition. tokens can create child tokens and a token
19
+ #can be associated with 0..1 parent- and 0..1 child- process instances. if a token does not
20
+ #have a parent token then it is the process instance's root token. in general, a token never
21
+ #waits in a node but immediately leaves the node after arrival. there are three exceptions:
22
+ #a token can create sub tokens which leave the node immediately (instead of the parent token).
23
+ #a token can wait in a state node (wait being signaled manually). and last but
24
+ #not least a node can defer execution to the root node of a newly created
25
+ #sub process and wait in a call node until the sub process has been finished. you can
26
+ #store :named variables in the token context. token variables can be involved in transition
27
+ #condition expression and can be get or set by event actions. if a token waits in a state
28
+ #the token variables can be manipulated by the workflow caller as well. but note that
29
+ #child tokens - and token variables - are destroyed upon arrival in a join node and
30
+ #that the root token is destroyed after the workflow has been finished.
31
+ class Token
32
+ attr_writer :sub_process, :parent_process
33
+ attr_reader :workflow, :node, :parent, :sub_process, :parent_process, :ref
34
+
35
+ def initialize(workflow, parent = nil, ctx_vars = {}, ref = -1)
36
+ @workflow = workflow
37
+ @parent = parent
38
+ @ctx_vars = ctx_vars
39
+ @ref = ref
40
+ @childs = []
41
+ @sub_process = nil
42
+ @parent_process = nil
43
+ @done = false
44
+ if parent
45
+ @node = parent.node
46
+ @parent.add_child(self)
47
+ else
48
+ @node = nil
49
+ end
50
+ end
51
+
52
+ def find_root_token
53
+ return parent.nil? ? self : parent.find_root_token
54
+ end
55
+
56
+ def find_super_token
57
+ return find_root_token.parent_process.nil? ? find_root_token : find_root_token.parent_process.find_super_token
58
+ end
59
+
60
+ def find_token(token_ref)
61
+ if @ref == token_ref
62
+ return self
63
+ elsif sub_process
64
+ return sub_process.find_token(token_ref)
65
+ else
66
+ return nil
67
+ end
68
+ end
69
+
70
+ def all_tokens(&blk)
71
+ blk.call(self)
72
+ if sub_process
73
+ sub_process.all_tokens(&blk)
74
+ end
75
+ end
76
+
77
+ def all_ctx_vars
78
+ @ctx_vars
79
+ end
80
+
81
+ #transition condition expressions are evaluated using token's binding()
82
+ #and this method is here to support expressions like "token[:var_name] == ..."
83
+ def token
84
+ self
85
+ end
86
+
87
+ #set a token variable
88
+ def []=(key, value)
89
+ @ctx_vars[key] = value
90
+ end
91
+
92
+ #get a token variable
93
+ def [](key)
94
+ @ctx_vars[key]
95
+ end
96
+
97
+ def add_child(token)
98
+ @childs << token
99
+ end
100
+
101
+ def remove_child(token)
102
+ @childs.delete(token)
103
+ end
104
+
105
+ #is this the parent of children tokens?
106
+ def childs?
107
+ not @childs.empty?
108
+ end
109
+
110
+ #sends a signal to a node waiting in a state.
111
+ #never call this method manually if the node
112
+ #does not wait in a state but does wait in
113
+ #a call node, because the node will be signalled
114
+ #automatically after the sub process instance
115
+ #has been finished.
116
+ def signal
117
+ @workflow.signal_node(self, node)
118
+
119
+ if end? && @parent_process
120
+ @parent_process.signal
121
+ @parent_process = nil
122
+ end
123
+ end
124
+
125
+ def enter(node)
126
+ @workflow.enter_node(self, node)
127
+ end
128
+
129
+ def entered(node)
130
+ @node = node
131
+
132
+ if @workflow.clazz.end_node?(@node) and not @parent
133
+ @workflow.token_done(self)
134
+ end
135
+ end
136
+
137
+ def transit(transition)
138
+ hardcoded_transition = @workflow.leave_node(self, @node)
139
+ if hardcoded_transition
140
+ @workflow.transit(self, @node, hardcoded_transition)
141
+ else
142
+ @workflow.transit(self, @node, transition)
143
+ end
144
+ end
145
+
146
+ def jump(child)
147
+ @node = child.node
148
+ end
149
+
150
+ def done
151
+ @parent.remove_child(self) if @parent
152
+ @parent.jump(self) if @parent
153
+ @workflow.token_done(self)
154
+ @done = true
155
+ end
156
+
157
+ def end?
158
+ @done || @workflow.clazz.end_node?(@node)
159
+ end
160
+
161
+ def fork(ctx_vars = {})
162
+ @workflow.create_token(self, ctx_vars)
163
+ end
164
+
165
+ def eval_expr(expr)
166
+ eval(expr, binding())
167
+ end
168
+ end
169
+ end