ruote-extras 0.9.18

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+