ruote-extras 0.9.18

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,739 @@
1
+ #
2
+ #--
3
+ # Copyright (c) 2007-2008, John Mettraux, Tomaso Tosolini OpenWFE.org
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # . Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # . Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # . Neither the name of the "OpenWFE" nor the names of its contributors may be
17
+ # used to endorse or promote products derived from this software without
18
+ # specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
+ # POSSIBILITY OF SUCH DAMAGE.
31
+ #++
32
+ #
33
+
34
+ #
35
+ # "made in Japan"
36
+ #
37
+ # John Mettraux at openwfe.org
38
+ # Tomaso Tosolini
39
+ #
40
+
41
+ #require 'rubygems'
42
+
43
+ #require_gem 'activerecord'
44
+ gem 'activerecord'; require 'active_record'
45
+
46
+
47
+ require 'openwfe/workitem'
48
+ require 'openwfe/flowexpressionid'
49
+ require 'openwfe/engine/engine'
50
+ require 'openwfe/participants/participant'
51
+
52
+
53
+ module OpenWFE
54
+ module Extras
55
+
56
+ #MUTEX = Mutex.new
57
+
58
+ #
59
+ # The migration for ActiveParticipant and associated classes.
60
+ #
61
+ # There are two tables 'workitems' and 'fields'. As its name implies,
62
+ # the latter table stores the fields (also called attributes in OpenWFE
63
+ # speak) of the workitems.
64
+ #
65
+ # See Workitem and Field for more details.
66
+ #
67
+ # For centralization purposes, the migration and the model are located
68
+ # in the same source file. It should be quite easy for the Rails hackers
69
+ # among you to sort that out for a Rails based usage.
70
+ #
71
+ class WorkitemTables < ActiveRecord::Migration
72
+
73
+ def self.up
74
+
75
+ create_table :workitems do |t|
76
+ t.column :fei, :string
77
+ t.column :wfid, :string
78
+ t.column :wf_name, :string
79
+ t.column :wf_revision, :string
80
+ t.column :participant_name, :string
81
+ t.column :store_name, :string
82
+ t.column :dispatch_time, :timestamp
83
+ t.column :last_modified, :timestamp
84
+
85
+ t.column :yattributes, :text
86
+ # when using compact_workitems, attributes are stored here
87
+ end
88
+ add_index :workitems, :fei, :unique => true
89
+ add_index :workitems, :wfid
90
+ add_index :workitems, :wf_name
91
+ add_index :workitems, :wf_revision
92
+ add_index :workitems, :participant_name
93
+ add_index :workitems, :store_name
94
+
95
+ create_table :fields do |t|
96
+ t.column :fkey, :string, :null => false
97
+ t.column :vclass, :string, :null => false
98
+ t.column :svalue, :string
99
+ t.column :yvalue, :text
100
+ t.column :workitem_id, :integer, :null => false
101
+ end
102
+ add_index :fields, [ :workitem_id, :fkey ], :unique => true
103
+ add_index :fields, :fkey
104
+ add_index :fields, :vclass
105
+ add_index :fields, :svalue
106
+ end
107
+
108
+ def self.down
109
+
110
+ drop_table :workitems
111
+ drop_table :fields
112
+ end
113
+ end
114
+
115
+ #
116
+ # Reopening InFlowWorkItem to add a 'db_id' attribute.
117
+ #
118
+ class OpenWFE::InFlowWorkItem
119
+
120
+ attr_accessor :db_id
121
+ end
122
+
123
+ #
124
+ # The ActiveRecord version of an OpenWFEru workitem (InFlowWorkItem).
125
+ #
126
+ # One can very easily build a worklist based on a participant name via :
127
+ #
128
+ # wl = OpenWFE::Extras::Workitem.find_all_by_participant_name("toto")
129
+ # puts "found #{wl.size} workitems for participant 'toto'"
130
+ #
131
+ # These workitems are not OpenWFEru workitems directly. But the conversion
132
+ # is pretty easy.
133
+ # Note that you probaly won't need to do the conversion by yourself,
134
+ # except for certain advanced scenarii.
135
+ #
136
+ # awi = OpenWFE::Extras::Workitem.find_by_participant_name("toto")
137
+ # #
138
+ # # returns the first workitem in the database whose participant
139
+ # # name is 'toto'.
140
+ #
141
+ # owi = awi.as_owfe_workitem
142
+ # #
143
+ # # Now we have a copy of the reference as a OpenWFEru
144
+ # # InFlowWorkItem instance.
145
+ #
146
+ # awi = OpenWFE::Extras::Workitem.from_owfe_workitem(owi)
147
+ # #
148
+ # # turns an OpenWFEru InFlowWorkItem instance into an
149
+ # # 'active workitem'.
150
+ #
151
+ class Workitem < ActiveRecord::Base
152
+
153
+ has_many :fields, :dependent => :destroy
154
+
155
+ serialize :yattributes
156
+
157
+
158
+ #
159
+ # Returns the flow expression id of this work (its unique OpenWFEru
160
+ # identifier) as a FlowExpressionId instance.
161
+ # (within the Workitem it's just stored as a String).
162
+ #
163
+ def full_fei
164
+
165
+ OpenWFE::FlowExpressionId.from_s(fei)
166
+ end
167
+
168
+ #
169
+ # Generates a (new) Workitem from an OpenWFEru InFlowWorkItem instance.
170
+ #
171
+ # This is a 'static' method :
172
+ #
173
+ # awi = OpenWFE::Extras::Workitem.from_owfe_workitem(wi)
174
+ #
175
+ # (This method saves the 'ActiveWorkitem').
176
+ #
177
+ def Workitem.from_owfe_workitem (wi, store_name=nil)
178
+
179
+ i = nil
180
+
181
+ #MUTEX.synchronize do
182
+
183
+ i = Workitem.new
184
+ i.fei = wi.fei.to_s
185
+ i.wfid = wi.fei.wfid
186
+ i.wf_name = wi.fei.workflow_definition_name
187
+ i.wf_revision = wi.fei.workflow_definition_revision
188
+ i.participant_name = wi.participant_name
189
+ i.dispatch_time = wi.dispatch_time
190
+ i.last_modified = nil
191
+
192
+ i.store_name = store_name
193
+
194
+ i.save!
195
+ # save workitem before adding any field
196
+ # making sure it has an id...
197
+
198
+
199
+ # This is a field set by the active participant immediately
200
+ # before calling this method.
201
+ # the default behavior is "use field method"
202
+
203
+ if wi.attributes["compact_workitems"]
204
+
205
+ wi.attributes.delete("compact_workitems")
206
+ i.yattributes = wi.attributes
207
+
208
+ else
209
+
210
+ i.yattributes = nil
211
+
212
+ wi.attributes.each do |k, v|
213
+ i.fields << Field.new_field(k, v)
214
+ end
215
+ end
216
+
217
+ i.save!
218
+ # making sure to throw an exception in case of trouble
219
+ #
220
+ # damn, insert then update :(
221
+
222
+ #end
223
+
224
+ i
225
+ end
226
+
227
+ #
228
+ # Turns the densha Workitem into an OpenWFEru InFlowWorkItem.
229
+ #
230
+ def as_owfe_workitem
231
+
232
+ wi = OpenWFE::InFlowWorkItem.new
233
+
234
+ wi.fei = full_fei
235
+ wi.participant_name = participant_name
236
+ wi.attributes = fields_hash
237
+
238
+ wi.dispatch_time = dispatch_time
239
+ wi.last_modified = last_modified
240
+
241
+ wi.db_id = self.id
242
+
243
+ wi
244
+ end
245
+
246
+ #
247
+ # Returns a hash version of the 'fields' of this workitem.
248
+ #
249
+ # (Each time this method is called, it returns a new hash).
250
+ #
251
+ def fields_hash
252
+
253
+ return self.yattributes if self.yattributes
254
+
255
+ fields.inject({}) do |r, f|
256
+ r[f.fkey] = f.value
257
+ r
258
+ end
259
+ end
260
+
261
+ #
262
+ # Replaces the current fields of this workitem with the given hash.
263
+ #
264
+ # This method modifies the content of the db.
265
+ #
266
+ def replace_fields (fhash)
267
+
268
+ if self.yattributes
269
+
270
+ self.yattributes = fhash
271
+
272
+ else
273
+
274
+ fields.delete_all
275
+
276
+ fhash.each do |k, v|
277
+ fields << Field.new_field(k, v)
278
+ end
279
+ end
280
+
281
+ #f = Field.new_field("___map_type", "smap")
282
+ #
283
+ # an old trick for backward compatibility with OpenWFEja
284
+
285
+ save!
286
+ # making sure to throw an exception in case of trouble
287
+ end
288
+
289
+ #
290
+ # Returns the Field instance with the given key. This method accept
291
+ # symbols as well as strings as its parameter.
292
+ #
293
+ # wi.field("customer_name")
294
+ # wi.field :customer_name
295
+ #
296
+ def field (key)
297
+
298
+ if self.yattributes
299
+ return self.yattributes[key.to_s]
300
+ end
301
+
302
+ fields.find_by_fkey key.to_s
303
+ end
304
+
305
+ #
306
+ # A shortcut method, replies to the workflow engine and removes self
307
+ # from the database.
308
+ # Handy for people who don't want to play with an ActiveParticipant
309
+ # instance when just consuming workitems (that an active participant
310
+ # pushed in the database).
311
+ #
312
+ def reply (engine)
313
+
314
+ engine.reply self.as_owfe_workitem
315
+ self.destroy
316
+ end
317
+
318
+ alias :forward :reply
319
+ alias :proceed :reply
320
+
321
+ #
322
+ # Simply sets the 'last_modified' field to now.
323
+ # (Doesn't save the workitem though).
324
+ #
325
+ def touch
326
+
327
+ self.last_modified = Time.now
328
+ end
329
+
330
+ #
331
+ # Opening engine to update its reply method to accept these
332
+ # active record workitems.
333
+ #
334
+ class OpenWFE::Engine
335
+
336
+ alias :oldreply :reply
337
+
338
+ def reply (workitem)
339
+
340
+ if workitem.is_a?(Workitem)
341
+
342
+ oldreply(workitem.as_owfe_workitem)
343
+ workitem.destroy
344
+ else
345
+
346
+ oldreply(workitem)
347
+ end
348
+ end
349
+
350
+ alias :forward :reply
351
+ alias :proceed :reply
352
+ end
353
+
354
+ #
355
+ # Returns all the workitems belonging to the stores listed
356
+ # in the parameter storename_list.
357
+ # The result is a Hash whose keys are the store names and whose
358
+ # values are list of workitems.
359
+ #
360
+ def self.find_in_stores (storename_list)
361
+
362
+ workitems = find_all_by_store_name(storename_list)
363
+
364
+ result = {}
365
+
366
+ workitems.each do |wi|
367
+ (result[wi.store_name] ||= []) << wi
368
+ end
369
+
370
+ result
371
+ end
372
+
373
+ #
374
+ # A kind of 'google search' among workitems
375
+ #
376
+ # == Note
377
+ #
378
+ # when this is used on compact_workitems, it will not be able to search
379
+ # info within the fields, because they aren't used by this kind of
380
+ # workitems. In this case the search will be limited to participant_name
381
+ #
382
+ def self.search (search_string, storename_list=nil)
383
+
384
+ #t = OpenWFE::Timer.new
385
+
386
+ storename_list = Array(storename_list) if storename_list
387
+
388
+ # participant_name
389
+
390
+ result = find(
391
+ :all,
392
+ :conditions => conditions(
393
+ "participant_name", search_string, storename_list),
394
+ :order => "participant_name")
395
+ # :limit => 10)
396
+
397
+ ids = result.collect { |wi| wi.id }
398
+
399
+ # search in fields
400
+
401
+ fields = Field.search search_string, storename_list
402
+ merge_search_results ids, result, fields
403
+
404
+ #puts "... took #{t.duration} ms"
405
+
406
+ # over.
407
+
408
+ result
409
+ end
410
+
411
+ #
412
+ # Not really about 'just launched', but rather about finding the first
413
+ # workitem for a given process instance (wfid) and a participant.
414
+ # It deserves its own method because the workitem could be in a
415
+ # subprocess, thus escaping the vanilla find_by_wfid_and_participant()
416
+ #
417
+ def self.find_just_launched (wfid, participant_name)
418
+
419
+ find(
420
+ :first,
421
+ :conditions => [
422
+ "wfid LIKE ? AND participant_name = ?",
423
+ "#{wfid}%",
424
+ participant_name ])
425
+ end
426
+
427
+ protected
428
+
429
+ #
430
+ # builds the condition (the WHERE clause) for the
431
+ # search.
432
+ #
433
+ def self.conditions (keyname, search_string, storename_list)
434
+
435
+ cs = [ "#{keyname} LIKE ?", search_string ]
436
+
437
+ if storename_list
438
+
439
+ cs[0] = "#{cs[0]} AND workitems.store_name IN (?)"
440
+ cs << storename_list
441
+ end
442
+
443
+ cs
444
+ end
445
+
446
+ def self.merge_search_results (ids, wis, new_wis)
447
+
448
+ return if new_wis.size < 1
449
+
450
+ new_wis.each do |wi|
451
+ wi = wi.workitem if wi.kind_of?(Field)
452
+ next if ids.include? wi.id
453
+ ids << wi.id
454
+ wis << wi
455
+ end
456
+ end
457
+ end
458
+
459
+ #
460
+ # A workaround is in place for some classes when then have to get
461
+ # serialized. The names of thoses classes are listed in this array.
462
+ #
463
+ SPECIAL_FIELD_CLASSES = [ 'Time', 'Date', 'DateTime' ]
464
+
465
+ #
466
+ # A Field (Attribute) of a Workitem.
467
+ #
468
+ class Field < ActiveRecord::Base
469
+
470
+ belongs_to :workitem
471
+ serialize :yvalue
472
+
473
+ #
474
+ # A quick method for doing
475
+ #
476
+ # f = Field.new
477
+ # f.key = key
478
+ # f.value = value
479
+ #
480
+ # One can then quickly add fields to an [active] workitem via :
481
+ #
482
+ # wi.fields << Field.new_field("toto", "b")
483
+ #
484
+ # This method does not save the new Field.
485
+ #
486
+ def self.new_field (key, value)
487
+
488
+ f = Field.new
489
+ f.fkey = key
490
+ f.vclass = value.class.to_s
491
+ f.value = value
492
+ f
493
+ end
494
+
495
+ def value= (v)
496
+
497
+ limit = connection.native_database_types[:string][:limit]
498
+
499
+ if v.is_a?(String) and v.length <= limit
500
+
501
+ self.svalue = v
502
+
503
+ elsif SPECIAL_FIELD_CLASSES.include?(v.class.to_s)
504
+
505
+ self.svalue = v.to_yaml
506
+
507
+ else
508
+
509
+ self.yvalue = v
510
+ end
511
+ end
512
+
513
+ def value
514
+
515
+ return YAML.load(self.svalue) \
516
+ if SPECIAL_FIELD_CLASSES.include?(self.vclass)
517
+
518
+ self.svalue || self.yvalue
519
+ end
520
+
521
+ #
522
+ # Will return all the fields that contain the given text.
523
+ #
524
+ # Looks in svalue and fkey. Looks as well in yvalue if it contains
525
+ # a string.
526
+ #
527
+ # This method is used by Workitem.search()
528
+ #
529
+ def self.search (text, storename_list=nil)
530
+
531
+ cs = build_search_conditions(text)
532
+
533
+ if storename_list
534
+
535
+ cs[0] = "(#{cs[0]}) AND workitems.store_name IN (?)"
536
+ cs << storename_list
537
+ end
538
+
539
+ find :all, :conditions => cs, :include => :workitem
540
+ end
541
+
542
+ protected
543
+
544
+ #
545
+ # The search operates on the content of these columns
546
+ #
547
+ FIELDS_TO_SEARCH = %w{ svalue fkey yvalue }
548
+
549
+ #
550
+ # Builds the condition array for a pseudo text search
551
+ #
552
+ def self.build_search_conditions (text)
553
+
554
+ has_percent = (text.index("%") != nil)
555
+
556
+ conds = []
557
+
558
+ conds << FIELDS_TO_SEARCH.collect { |key|
559
+
560
+ count = has_percent ? 1 : 4
561
+
562
+ s = ([ "#{key} LIKE ?" ] * count).join(" OR ")
563
+
564
+ s = "(vclass = ? AND (#{s}))" if key == 'yvalue'
565
+
566
+ s
567
+ }.join(" OR ")
568
+
569
+ FIELDS_TO_SEARCH.each do |key|
570
+
571
+ conds << 'String' if key == 'yvalue'
572
+
573
+ conds << text
574
+
575
+ unless has_percent
576
+ conds << "% #{text} %"
577
+ conds << "% #{text}"
578
+ conds << "#{text} %"
579
+ end
580
+ end
581
+
582
+ conds
583
+ end
584
+ end
585
+
586
+
587
+ #
588
+ # A basic 'ActiveParticipant'.
589
+ # A store participant whose store is a set of ActiveRecord tables.
590
+ #
591
+ # Sample usage :
592
+ #
593
+ # class MyDefinition < OpenWFE::ProcessDefinition
594
+ # sequence do
595
+ # active0
596
+ # active1
597
+ # end
598
+ # end
599
+ #
600
+ # def play_with_the_engine
601
+ #
602
+ # engine = OpenWFE::Engine.new
603
+ #
604
+ # engine.register_participant(
605
+ # :active0, OpenWFE::Extras::ActiveParticipant)
606
+ # engine.register_participant(
607
+ # :active1, OpenWFE::Extras::ActiveParticipant)
608
+ #
609
+ # li = OpenWFE::LaunchItem.new(MyDefinition)
610
+ # li.customer_name = 'toto'
611
+ # engine.launch li
612
+ #
613
+ # sleep 0.500
614
+ # # give some slack to the engine, it's asynchronous after all
615
+ #
616
+ # wi = OpenWFE::Extras::Workitem.find_by_participant_name("active0")
617
+ #
618
+ # # ...
619
+ # end
620
+ #
621
+ # == Compact workitems
622
+ #
623
+ # It is possible to save all the workitem data into a single table,
624
+ # the workitems table, without
625
+ # splitting info between workitems and fields tables.
626
+ #
627
+ # You can configure the "compact_workitems" behavior by adding to the
628
+ # previous lines:
629
+ #
630
+ # active0 = engine.register_participant(
631
+ # :active0, OpenWFE::Extras::ActiveParticipant)
632
+ #
633
+ # active0.compact_workitems = true
634
+ #
635
+ # This behaviour is determined participant per participant, it's ok to
636
+ # have a participant instance that compacts will there is another that
637
+ # doesn't compact.
638
+ #
639
+ class ActiveParticipant
640
+ include OpenWFE::LocalParticipant
641
+
642
+ #
643
+ # when compact_workitems is set to true, the attributes of a workitem
644
+ # are stored in the yattributes column (they are not expanded into
645
+ # the Fields table).
646
+ # By default, workitem attributes are expanded.
647
+ #
648
+ attr :compact_workitems, true
649
+
650
+ #
651
+ # This is the method called by the OpenWFEru engine to hand a
652
+ # workitem to this participant.
653
+ #
654
+ def consume (workitem)
655
+
656
+ if compact_workitems
657
+ workitem.attributes["compact_workitems"] = true
658
+ end
659
+
660
+ Workitem.from_owfe_workitem workitem
661
+ end
662
+
663
+ #
664
+ # Called by the engine when the whole process instance (or just the
665
+ # segment of it that sports this participant) is cancelled.
666
+ # Will removed the workitem with the same fei as the cancelitem
667
+ # from the database.
668
+ #
669
+ # No expression will be raised if there is no corresponding workitem.
670
+ #
671
+ def cancel (cancelitem)
672
+
673
+ Workitem.delete_all([ "fei = ?", cancelitem.fei.to_s ])
674
+ end
675
+
676
+ #
677
+ # When the activity/work/operation whatever is over and the flow
678
+ # should resume, this is the method to use to hand back the [modified]
679
+ # workitem to the [local] engine.
680
+ #
681
+ def reply_to_engine (workitem)
682
+
683
+ super workitem.as_owfe_workitem
684
+ #
685
+ # replies to the workflow engine
686
+
687
+ workitem.destroy
688
+ #
689
+ # removes the workitem from the database
690
+ end
691
+ end
692
+
693
+ #
694
+ # An extension of ActiveParticipant. It has a 'store_name' and it
695
+ # makes sure to flag every workitem it 'consumes' with that name
696
+ # (in its 'store_name' column/field).
697
+ #
698
+ # This is the participant used mainly in 'densha' for human users.
699
+ #
700
+ class ActiveStoreParticipant < ActiveParticipant
701
+ include Enumerable
702
+
703
+ def initialize (store_name)
704
+
705
+ super()
706
+ @store_name = store_name
707
+ end
708
+
709
+ #
710
+ # This is the method called by the OpenWFEru engine to hand a
711
+ # workitem to this participant.
712
+ #
713
+ def consume (workitem)
714
+
715
+ if compact_workitems
716
+ workitem.attributes["compact_workitems"] = true
717
+ end
718
+
719
+ Workitem.from_owfe_workitem(workitem, @store_name)
720
+ end
721
+
722
+ #
723
+ # Iterates over the workitems currently in this store.
724
+ #
725
+ def each (&block)
726
+
727
+ return unless block
728
+
729
+ wis = Workitem.find_by_store_name @store_name
730
+
731
+ wis.each do |wi|
732
+ block.call wi
733
+ end
734
+ end
735
+ end
736
+
737
+ end
738
+ end
739
+