caruby-core 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +51 -0
  5. data/doc/website/css/site.css +1 -5
  6. data/doc/website/images/avatar.png +0 -0
  7. data/doc/website/images/favicon.ico +0 -0
  8. data/doc/website/images/logo.png +0 -0
  9. data/doc/website/index.html +82 -0
  10. data/doc/website/install.html +87 -0
  11. data/doc/website/quick_start.html +87 -0
  12. data/doc/website/tissue.html +85 -0
  13. data/doc/website/uom.html +10 -0
  14. data/lib/caruby.rb +3 -0
  15. data/lib/caruby/active_support/README.txt +2 -0
  16. data/lib/caruby/active_support/core_ext/string.rb +7 -0
  17. data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
  18. data/lib/caruby/active_support/inflections.rb +55 -0
  19. data/lib/caruby/active_support/inflector.rb +398 -0
  20. data/lib/caruby/cli/application.rb +36 -0
  21. data/lib/caruby/cli/command.rb +169 -0
  22. data/lib/caruby/csv/csv_mapper.rb +157 -0
  23. data/lib/caruby/csv/csvio.rb +185 -0
  24. data/lib/caruby/database.rb +252 -0
  25. data/lib/caruby/database/fetched_matcher.rb +66 -0
  26. data/lib/caruby/database/persistable.rb +432 -0
  27. data/lib/caruby/database/persistence_service.rb +162 -0
  28. data/lib/caruby/database/reader.rb +599 -0
  29. data/lib/caruby/database/saved_merger.rb +131 -0
  30. data/lib/caruby/database/search_template_builder.rb +59 -0
  31. data/lib/caruby/database/sql_executor.rb +75 -0
  32. data/lib/caruby/database/store_template_builder.rb +200 -0
  33. data/lib/caruby/database/writer.rb +469 -0
  34. data/lib/caruby/domain/annotatable.rb +25 -0
  35. data/lib/caruby/domain/annotation.rb +23 -0
  36. data/lib/caruby/domain/attribute_metadata.rb +447 -0
  37. data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
  38. data/lib/caruby/domain/merge.rb +91 -0
  39. data/lib/caruby/domain/properties.rb +95 -0
  40. data/lib/caruby/domain/reference_visitor.rb +289 -0
  41. data/lib/caruby/domain/resource_attributes.rb +528 -0
  42. data/lib/caruby/domain/resource_dependency.rb +205 -0
  43. data/lib/caruby/domain/resource_introspection.rb +159 -0
  44. data/lib/caruby/domain/resource_metadata.rb +117 -0
  45. data/lib/caruby/domain/resource_module.rb +285 -0
  46. data/lib/caruby/domain/uniquify.rb +38 -0
  47. data/lib/caruby/import/annotatable_class.rb +28 -0
  48. data/lib/caruby/import/annotation_class.rb +27 -0
  49. data/lib/caruby/import/annotation_module.rb +67 -0
  50. data/lib/caruby/import/java.rb +338 -0
  51. data/lib/caruby/migration/migratable.rb +167 -0
  52. data/lib/caruby/migration/migrator.rb +533 -0
  53. data/lib/caruby/migration/resource.rb +8 -0
  54. data/lib/caruby/migration/resource_module.rb +11 -0
  55. data/lib/caruby/migration/uniquify.rb +20 -0
  56. data/lib/caruby/resource.rb +969 -0
  57. data/lib/caruby/util/attribute_path.rb +46 -0
  58. data/lib/caruby/util/cache.rb +53 -0
  59. data/lib/caruby/util/class.rb +99 -0
  60. data/lib/caruby/util/collection.rb +1053 -0
  61. data/lib/caruby/util/controlled_value.rb +35 -0
  62. data/lib/caruby/util/coordinate.rb +75 -0
  63. data/lib/caruby/util/domain_extent.rb +49 -0
  64. data/lib/caruby/util/file_separator.rb +65 -0
  65. data/lib/caruby/util/inflector.rb +20 -0
  66. data/lib/caruby/util/log.rb +95 -0
  67. data/lib/caruby/util/math.rb +12 -0
  68. data/lib/caruby/util/merge.rb +59 -0
  69. data/lib/caruby/util/module.rb +34 -0
  70. data/lib/caruby/util/options.rb +92 -0
  71. data/lib/caruby/util/partial_order.rb +36 -0
  72. data/lib/caruby/util/person.rb +119 -0
  73. data/lib/caruby/util/pretty_print.rb +184 -0
  74. data/lib/caruby/util/properties.rb +112 -0
  75. data/lib/caruby/util/stopwatch.rb +66 -0
  76. data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
  77. data/lib/caruby/util/transitive_closure.rb +45 -0
  78. data/lib/caruby/util/tree.rb +48 -0
  79. data/lib/caruby/util/trie.rb +37 -0
  80. data/lib/caruby/util/uniquifier.rb +30 -0
  81. data/lib/caruby/util/validation.rb +48 -0
  82. data/lib/caruby/util/version.rb +56 -0
  83. data/lib/caruby/util/visitor.rb +351 -0
  84. data/lib/caruby/util/weak_hash.rb +36 -0
  85. data/lib/caruby/version.rb +3 -0
  86. metadata +186 -0
@@ -0,0 +1,252 @@
1
+ require 'generator'
2
+ require 'caruby/util/log'
3
+ require 'caruby/util/collection'
4
+ require 'caruby/util/validation'
5
+ require 'caruby/util/options'
6
+ require 'caruby/util/visitor'
7
+ require 'caruby/util/inflector'
8
+ require 'caruby/database/persistable'
9
+ require 'caruby/database/reader'
10
+ require 'caruby/database/writer'
11
+ require 'caruby/database/persistence_service'
12
+
13
+ # the caBIG client classes
14
+ import 'gov.nih.nci.system.applicationservice.ApplicationServiceProvider'
15
+ import 'gov.nih.nci.system.comm.client.ClientSession'
16
+
17
+ module CaRuby
18
+ # Database operation error.
19
+ class DatabaseError < RuntimeError; end
20
+
21
+ # A Database mediates access to a caBIG database. Database is a facade for caBIG application service
22
+ # database operations. Database supports the query, create, update and delete operations supported by
23
+ # the application service.
24
+ #
25
+ # Database strives to provide a simple WYEIWYG (What You Expect Is What You Get) API, consisting of
26
+ # the following workhorse methods:
27
+ # * {Query#query} - fetch domain objects which match a template
28
+ # * {Store#find} - fetch a specific domain object by key
29
+ # * {Store#store} - if a domain object exists in the database, then update it, otherwise create it
30
+ #
31
+ # Any domain object can serve as a query argument. If an optional attribute path is specified, then
32
+ # that path is followed to the result, e.g.:
33
+ # database.query(study, :coordinator)
34
+ # returns the coordinators of studies which match the +study+ template.
35
+ #
36
+ # A domain object find argument must contain enough data to determine whether it exists in the database,
37
+ # i.e. the find argument has a database identifier or a complete secondary key.
38
+ #
39
+ # The {Store#store} method creates or updates references as necessary to persist its argument domain object.
40
+ # It is not necessary to fetch references first or follow dependency ordering rules, which can be
41
+ # implicit and tortuous in caBIG applications. Build the object you want to persist and call the
42
+ # store method. CaRuby::Resource sets reasonable default values, recognizes application dependencies and steers
43
+ # around caBIG idiosyncracies to the extent possible.
44
+ class Database
45
+ include Reader, Writer, Validation
46
+
47
+ attr_reader :operations
48
+
49
+ # Creates a new Database with the specified service name and options.
50
+ #
51
+ # @param [String] service_name the name of the default {PersistenceService}
52
+ # @param [{Symbol => String}] opts access options
53
+ # @option opts [String] :login application service login user
54
+ # @option opts [String] :password application service login password
55
+ # @example
56
+ # Database.new(:user => 'perdita', :password => 'changeMe')
57
+ def initialize(service_name, opts)
58
+ super()
59
+ # the fetched object cache
60
+ @cache = create_cache
61
+ @defaults = {}
62
+ @user = Options.get(:user, opts)
63
+ @password = Options.get(:password, opts)
64
+ @host = Options.get(:host, opts)
65
+ # class => service hash; default is the catissuecore app service
66
+ @def_persist_svc = PersistenceService.new(service_name, :host => @host)
67
+ @cls_svc_hash = Hash.new(@def_persist_svc)
68
+ # the create/update nested operations
69
+ @operations = []
70
+ # the objects for which exists? is unsuccessful in the context of a nested operation
71
+ @transients = Set.new
72
+ end
73
+
74
+ # Calls the block given to this method with this database as an argument, and closes the
75
+ # database when done.
76
+ #
77
+ # @yield [database] the operation to perform on the database
78
+ # @yieldparam [Database] database self
79
+ def open
80
+ # reset the execution timers
81
+ persistence_services.each { |svc| svc.timer.reset }
82
+ # call the block and close when done
83
+ yield(self) ensure close
84
+ end
85
+
86
+ # Releases database resources. This method should be called when database interaction
87
+ # is completed.
88
+ def close
89
+ return if @session.nil?
90
+ begin
91
+ @session.terminate_session
92
+ rescue Exception => e
93
+ logger.error("Session termination unsuccessful - #{e.message}")
94
+ end
95
+ # clear the cache
96
+ @cache.clear
97
+ logger.info("Disconnected from application server.")
98
+ @session = nil
99
+ end
100
+
101
+ # Returns the execution time spent since the last open.
102
+ def execution_time
103
+ persistence_services.inject(0) do |total, svc|
104
+ st = svc.timer.elapsed
105
+ total + st
106
+ end
107
+ end
108
+
109
+ # Returns the PersistanceService to use for the given domain object obj,
110
+ # or the default service if obj is nil.
111
+ def persistence_service(obj=nil)
112
+ start_session if @session.nil?
113
+ return @def_persist_svc if obj.nil?
114
+ klass = Class === obj ? obj : obj.class
115
+ @cls_svc_hash[klass]
116
+ end
117
+
118
+ # Returns all PersistanceServices used by this database.
119
+ def persistence_services
120
+ [@def_persist_svc].to_set.merge!(@cls_svc_hash.values)
121
+ end
122
+
123
+ # Returns the database operation elapsed real time since the last open.
124
+ def database_time
125
+ persistence_services.inject(0) do |total, svc|
126
+ st = svc.timer.elapsed
127
+ # reset the timer for the next test case
128
+ svc.timer.reset
129
+ total + st
130
+ end
131
+ end
132
+
133
+ # A mergeable autogenerated operation is recursively defined as:
134
+ # * a create
135
+ # * an update in the context of a mergeable autogenerated operation
136
+ #
137
+ # @return whether the innermost operation conforms to the above criterion
138
+ def mergeable_autogenerated_operation?
139
+ # the inner operation subject
140
+ inner = nil
141
+ @operations.reverse_each do |op|
142
+ if inner and op.subject != inner.owner then
143
+ # not a dependent
144
+ return false
145
+ end
146
+ if op.type == :create then
147
+ # innermost or owner create
148
+ return true
149
+ elsif op.type != :update then
150
+ # not a save
151
+ return false
152
+ end
153
+ # iterate to the scoping operation
154
+ inner = op.subject
155
+ end
156
+ false
157
+ end
158
+
159
+ alias :to_s :print_class_and_id
160
+
161
+ alias :inspect :to_s
162
+
163
+ private
164
+
165
+ ## Utility classes and methods, used by Query and Store mix-ins ##
166
+
167
+ # Database CRUD operation.
168
+ class Operation
169
+ attr_reader :type, :subject, :attribute
170
+
171
+ def initialize(type, subject, attribute=nil)
172
+ @type = type
173
+ @subject = subject
174
+ @attribute = attribute
175
+ end
176
+ end
177
+
178
+ # Performs the operation given by the given op symbol on obj by calling the block given to this method.
179
+ # Returns the result of calling the block.
180
+ # Valid op symbols are described in {Operation#initialize}.
181
+ def perform(op, obj, attribute=nil)
182
+ op_s = op.to_s.capitalize_first
183
+ attr_s = " #{attribute}" if attribute
184
+ ctxt_s = " in context #{print_operations}" unless @operations.empty?
185
+ logger.info(">> #{op_s} #{obj.pp_s(:single_line)}#{attr_s}#{ctxt_s}...")
186
+ @operations.push(Operation.new(op, obj, attribute))
187
+ begin
188
+ # perform the operation
189
+ result = yield
190
+ ensure
191
+ # the operation is done
192
+ @operations.pop
193
+ # If this is a top-level operation, then clear the cache and transient set.
194
+ if @operations.empty? then
195
+ @cache.clear
196
+ @transients.clear
197
+ end
198
+ end
199
+ logger.info("<< Completed #{obj.qp}#{attr_s} #{op}.")
200
+ result
201
+ end
202
+
203
+ # @return [Cache] a new object cache.
204
+ def create_cache
205
+ # JRuby alert - identifier is not a stable object when fetched from the database, i.e.:
206
+ # obj.identifier.equal?(obj.identifier) #=> false
207
+ # This is probably an artifact of jRuby Numeric - Java Long conversion interaction
208
+ # combined with hash access use of the eql? method. Work-around is to make a Ruby Integer.
209
+ # the fetched object copier
210
+ copier = Proc.new do |src|
211
+ copy = src.copy
212
+ logger.debug { "Fetched #{src.qp} copied to #{copy.qp}." }
213
+ copy
214
+ end
215
+ # the fetched object cache
216
+ Cache.new(copier) do |obj|
217
+ raise ArgumentError.new("Can't cache object without identifier: #{obj}") unless obj.identifier
218
+ obj.identifier.to_s.to_i
219
+ end
220
+ end
221
+
222
+ # Initializes the default application service.
223
+ def start_session
224
+ raise DatabaseError.new('Application user option missing') if @user.nil?
225
+ raise DatabaseError.new('Application password option missing') if @password.nil?
226
+ # caCORE alert - obtaining a caCORE session instance mysteriously depends on referencing the application service first
227
+ @def_persist_svc.app_service
228
+ @session = ClientSession.instance()
229
+ connect(@user, @password)
230
+ end
231
+
232
+ # Returns the current database operation stack as a String.
233
+ def print_operations
234
+ ops = @operations.reverse.map do |op|
235
+ attr_s = " #{op.attribute}" if op.attribute
236
+ "#{op.type.to_s.capitalize_first} #{op.subject.qp}#{attr_s}"
237
+ end
238
+ ops.qp
239
+ end
240
+
241
+ # Connects to the database.
242
+ def connect(user, password)
243
+ logger.debug { "Connecting to application server with login id #{user}..." }
244
+ begin
245
+ @session.start_session(user, password)
246
+ rescue Exception => e
247
+ logger.error("Login of #{user} unsuccessful - #{e.message}") and raise
248
+ end
249
+ logger.info("Connected to application server.")
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,66 @@
1
+ require 'caruby/util/options'
2
+ require 'caruby/util/collection'
3
+ require 'caruby/util/cache'
4
+ require 'caruby/util/pretty_print'
5
+ require 'caruby/domain/reference_visitor'
6
+ require 'caruby/database/search_template_builder'
7
+
8
+ module CaRuby
9
+ class Database
10
+ # Proc that matches fetched sources to targets.
11
+ class FetchedMatcher < Proc
12
+ # Initializes a new FetchedMatcher.
13
+ #
14
+ # @param [{Symbol => Object}, Symbol, nil] opts the match options
15
+ # @option [Boolean] opts :relaxed flag indicating whether a {Resource#minimal_match?} is
16
+ # used in the match on the fetched content
17
+ def initialize(opts=nil)
18
+ super() { |srcs, tgts| match_fetched(srcs, tgts) }
19
+ @relaxed = Options.get(:relaxed, opts)
20
+ end
21
+
22
+ private
23
+
24
+ # Returns a target => source match hash for the given targets and sources.
25
+ def match_fetched(sources, targets)
26
+ return Hash::EMPTY_HASH if sources.empty? or targets.empty?
27
+ logger.debug { "Matching database content #{sources.qp} to #{targets.qp}..." }
28
+
29
+ # match source => target based on the key
30
+ unmatched = Set === sources ? sources.dup : sources.to_set
31
+ matches = {}
32
+ targets.each do |tgt|
33
+ src = tgt.match_in_owner_scope(unmatched)
34
+ next unless src
35
+ matches[src] = tgt
36
+ unmatched.delete(src)
37
+ end
38
+
39
+ # match residual targets, if any, on a relaxed criterion
40
+ if @relaxed and matches.size != targets.size then
41
+ unmtchd_tgts = targets.to_set - matches.keys.delete_if { |tgt| tgt.identifier }
42
+ unmtchd_srcs = sources.to_set - matches.values
43
+ min_mtchs = match_minimal(unmtchd_srcs, unmtchd_tgts)
44
+ matches.merge!(min_mtchs)
45
+ end
46
+
47
+ logger.debug { "Matched database sources to targets #{matches.qp}." } unless matches.empty?
48
+ matches
49
+ end
50
+
51
+ #@param [<Resource>] sources the source objects to match
52
+ #@param [<Resource>] targets the potential match target objects
53
+ # @return (see #match_saved)
54
+ def match_minimal(sources, targets)
55
+ matches = {}
56
+ unmatched = Set === sources ? sources.to_set : sources.dup
57
+ targets.each do |tgt|
58
+ src = unmatched.detect { |src| tgt.minimal_match?(src) } || next
59
+ matches[src] = tgt
60
+ unmatched.delete(src)
61
+ end
62
+ matches
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,432 @@
1
+ require 'caruby/util/log'
2
+ require 'caruby/util/pretty_print'
3
+ require 'caruby/util/inflector'
4
+ require 'caruby/util/collection'
5
+ require 'caruby/util/validation'
6
+
7
+ module CaRuby
8
+ # The Persistable mixin adds persistance capability.
9
+ module Persistable
10
+ include Validation
11
+
12
+ # @return [LazyLoader] the loader which fetches references on demand
13
+ attr_reader :lazy_loader
14
+
15
+ # @return [{Symbol => Object}] the content value hash at the point of the last
16
+ # take_snapshot call
17
+ attr_reader :snapshot
18
+
19
+ # @return [#query, #find, #store, #create, #update, #delete] the data access mediator
20
+ # for this Persistable
21
+ # @raise [NotImplementedError] if the subclass does not define this method
22
+ def database
23
+ raise NotImplementedError.new("Database operations are not available for #{self}")
24
+ end
25
+
26
+ # Fetches the domain objects which match this template from the {#database}.
27
+ #
28
+ # @param path (see Reader#query)
29
+ # @return (see Reader#query)
30
+ # @raise (see #database)
31
+ # @raise (see Reader#query)
32
+ # @see Reader#query
33
+ def query(*path)
34
+ path.empty? ? database.query(self) : database.query(self, *path)
35
+ end
36
+
37
+ # Fetches this domain object from the {#database}.
38
+ #
39
+ # @param opts (see Reader#find)
40
+ # @option (see Reader#find)
41
+ # @return (see Reader#find)
42
+ # @raise (see #database)
43
+ # @raise (see Reader#find)
44
+ # @see Reader#find
45
+ def find(opts=nil)
46
+ database.find(self, opts)
47
+ end
48
+
49
+ # Creates this domain object in the {#database}.
50
+ #
51
+ # @return (see Writer#create)
52
+ # @raise (see #database)
53
+ # @raise (see Writer#create)
54
+ # @see Writer#create
55
+ def create
56
+ database.create(self)
57
+ end
58
+
59
+ # Saves this domain object in the {#database}.
60
+ #
61
+ # @return (see Writer#save)
62
+ # @raise (see #database)
63
+ # @raise (see Writer#save)
64
+ # @see Writer#save
65
+ def save
66
+ database.save(self)
67
+ end
68
+
69
+ alias :store :save
70
+
71
+ # Updates this domain object in the {#database}.
72
+ #
73
+ # @return (see Writer#update)
74
+ # @raise (see #database)
75
+ # @raise (see Writer#update)
76
+ # @see Writer#update
77
+ def update
78
+ database.update(self)
79
+ end
80
+
81
+ # Deletes this domain object from the {#database}.
82
+ #
83
+ # @return (see Writer#delete)
84
+ # @raise (see #database)
85
+ # @raise (see Writer#delete)
86
+ # @see Writer#delete
87
+ def delete
88
+ database.delete(self)
89
+ end
90
+
91
+ alias :== :equal?
92
+
93
+ alias :eql? :==
94
+
95
+ # Captures the Persistable's updatable attribute base values.
96
+ # The snapshot is subsequently accessible using the {#snapshot} method.
97
+ #
98
+ # @return [{Symbol => Object}] the snapshot value hash
99
+ def take_snapshot
100
+ @snapshot = value_hash(self.class.updatable_attributes)
101
+ end
102
+
103
+ # @return [Boolean] whether this Persistable has a {#snapshot}
104
+ def snapshot_taken?
105
+ not @snapshot.nil?
106
+ end
107
+
108
+ # Returns whether this Persistable either doesn't have a snapshot or has changed since the last snapshot.
109
+ # This is a conservative condition test that returns false if there is no snaphsot for this Persistable
110
+ # and therefore no basis to determine whether the content changed.
111
+ #
112
+ # @return [Boolean] whether this Persistable's content differs from its snapshot
113
+ def changed?
114
+ @snapshot.nil? or not snapshot_equal_content?
115
+ end
116
+
117
+ # @return [<Symbol>] the attributes which differ between the {#snapshot} and current content
118
+ def changed_attributes
119
+ if @snapshot then
120
+ ovh = value_hash(self.class.updatable_attributes)
121
+ @snapshot.diff(ovh).keys
122
+ else
123
+ self.class.updatable_attributes
124
+ end
125
+ end
126
+
127
+ # Lazy loads the attributes. If a block is given to this method, then the attributes are determined
128
+ # by calling the block with this Persistable as a parameter. Otherwise, the default attributes
129
+ # are the unfetched domain attributes.
130
+ #
131
+ # Each of the attributes which does not already hold a non-nil or non-empty value
132
+ # will be loaded from the database on demand.
133
+ # This method injects attribute value initialization into each loadable attribute reader.
134
+ # The initializer is given by either the loader Proc argument or the block provided
135
+ # to this method. The loader takes two arguments, the target object and the attribute to load.
136
+ # If this Persistable already has a lazy loader, then this method is a no-op.
137
+ #
138
+ # Lazy loading is disabled on an attribute after it is invoked on that attribute or when the
139
+ # attribute setter method is called.
140
+ #
141
+ # @param loader [LazyLoader] the lazy loader to add
142
+ # @yield [sources, targets] source => target matcher
143
+ # @yieldparam [<Resource>] sources the fetched domain object match candidates
144
+ # @yieldparam [<Resource>] targets the search target domain objects to match
145
+ # @raise [ValidationError] if this domain object does not have an identifier
146
+ def add_lazy_loader(loader, &matcher)
147
+ # guard against invalid call
148
+ raise ValidationError.new("Cannot add lazy loader to an unfetched domain object: #{self}") if identifier.nil?
149
+ # no-op if there is already a loader
150
+ return if @lazy_loader
151
+
152
+ # the attributes to lazy-load
153
+ attrs = self.class.loadable_attributes
154
+ return if attrs.empty?
155
+
156
+ # make the lazy loader
157
+ @lazy_loader = LazyLoader.new(self, loader, &matcher)
158
+ # define the reader and writer method overrides for the missing attributes
159
+ loaded = attrs.select { |attr| persistable__add_loader(attr) }
160
+ logger.debug { "Lazy loader added to #{qp} attributes #{loaded.to_series}." } unless loaded.empty?
161
+ end
162
+
163
+ # Returns the attributes to load on demand. The base attribute list is given by
164
+ # {ResourceAttributes#loadable_attributes}. In additon, if this Persistable has
165
+ # more than one {ResourceDependency#owner_attributes} and one is non-nil, then
166
+ # none of the owner attributes are loaded on demand, since there can be at most
167
+ # one owner and ownership cannot change.
168
+ #
169
+ # @return [<Symbol>] the attributes to load on demand
170
+ def loadable_attributes
171
+ ownr_attrs = self.class.owner_attributes
172
+ if ownr_attrs.size == 2 and ownr_attrs.detect { |attr| send(ownr_attr) } then
173
+ self.class.loadable_attributes - ownr_attrs
174
+ else
175
+ self.class.loadable_attributes
176
+ end
177
+ end
178
+
179
+ # Disables this Persistable's lazy loader, if one exists. If a block is given to this
180
+ # method, then the loader is only disabled while the block is executed.
181
+ #
182
+ # @yield the block to call while the loader is suspended
183
+ # @return the result of calling the block, or self if no block is given
184
+ def suspend_lazy_loader
185
+ unless @lazy_loader and @lazy_loader.enabled? then
186
+ return block_given? ? yield : self
187
+ end
188
+ @lazy_loader.disable
189
+ return self unless block_given?
190
+ begin
191
+ yield
192
+ ensure
193
+ @lazy_loader.enable
194
+ end
195
+ end
196
+
197
+ # Enables this Persistable's lazy loader, if one exists. If a block is given to this
198
+ # method, then the loader is only enabled while the block is executed.
199
+ #
200
+ # @yield the block to call while the loader is enabled
201
+ # @return the result of calling the block, or self if no block is given
202
+ def resume_lazy_loader
203
+ unless @lazy_loader and @lazy_loader.disabled? then
204
+ return block_given? ? yield : self
205
+ end
206
+ @lazy_loader.enable
207
+ return self unless block_given?
208
+ begin
209
+ yield
210
+ ensure
211
+ @lazy_loader.disable
212
+ end
213
+ end
214
+
215
+ # Disables lazy loading of the specified attribute. Lazy loaded is disabled for all attributes
216
+ # if no attribute is specified. This method is a no-op if this Persistable does not have a lazy
217
+ # loader.
218
+ #
219
+ # @param [Symbol] the attribute to remove from the load list, or nil if to remove all attributes
220
+ def remove_lazy_loader(attribute=nil)
221
+ return if @lazy_loader.nil?
222
+ if attribute.nil? then
223
+ self.class.domain_attributes.each { |attr| remove_lazy_loader(attr) }
224
+ @lazy_loader = nil
225
+ return
226
+ end
227
+
228
+ # the modified accessor method
229
+ reader, writer = self.class.attribute_metadata(attribute).accessors
230
+ # remove the reader override
231
+ disable_singleton_method(reader)
232
+ # remove the writer override
233
+ disable_singleton_method(writer)
234
+ end
235
+
236
+ # Returns whether this domain object must be fetched to reflect the database state.
237
+ # This default implementation returns whether there are any autogenerated attributes.
238
+ # Subclasses can override with more restrictive conditions.
239
+ #
240
+ # caBIG alert - the auto-generated criterion is a sufficient but not necessary condition
241
+ # to determine whether a save caCORE result does not necessarily accurately reflect the
242
+ # database state. Examples:
243
+ # * caTissue SCG name is auto-generated on SCG create but not SCG update.
244
+ # * caTissue SCG event parameters are not auto-generated on SCG create if the SCG collection
245
+ # status is Pending, but are auto-generated on SCG update if the SCG status is changed
246
+ # to Complete.
247
+ # The caBIG application can override this method in a Database subclass to fine-tune the
248
+ # fetch criteria. Adding a more restrictive {#fetch_saved?} condition will will improve
249
+ # performance but not change functionality.
250
+ #
251
+ # @return [Boolean] whether this domain object must be fetched to reflect the database state
252
+ def fetch_saved?
253
+ not self.class.autogenerated_attributes.empty?
254
+ end
255
+
256
+ # Sets the {ResourceAttributes#volatile_nondomain_attributes} to the other fetched value,
257
+ # if different.
258
+ #
259
+ # @param [Resource] other the fetched domain object reflecting the database state
260
+ def copy_volatile_attributes(other)
261
+ self.class.volatile_nondomain_attributes.each do |attr|
262
+ val = send(attr)
263
+ oval = other.send(attr)
264
+ # set the attribute to the other value if it differs from the current value
265
+ unless oval == val then
266
+ # if this error occurs, then there is a serious match-merge flaw
267
+ if val and attr == :identifier then
268
+ raise DatabaseError.new("Can't copy #{other} to #{self} with different identifier")
269
+ end
270
+ # overwrite the current attribute value
271
+ set_attribute(attr, oval)
272
+ logger.debug { "Set #{qp} volatile #{attr} to the fetched #{other.qp} database value #{oval.qp}." }
273
+ end
274
+ end
275
+ end
276
+
277
+ private
278
+
279
+ # Returns whether the {#snapshot} and current content are equal.
280
+ # The attribute values _v_ and _ov_ of the snapshot and current content, resp., are
281
+ # compared with equality determined by {Resource.value_equal?}.
282
+ #
283
+ # @return [Boolean] whether the {#snapshot} and current content are equal
284
+ def snapshot_equal_content?
285
+ vh = @snapshot
286
+ ovh = value_hash(self.class.updatable_attributes)
287
+ if vh.size < ovh.size then
288
+ attr, oval = ovh.detect { |a, v| not vh.has_key?(a) }
289
+ logger.debug { "#{qp} is missing snapshot #{attr} compared to the current value #{oval.qp}." }
290
+ false
291
+ elsif vh.size > ovh.size then
292
+ attr, value = vh.detect { |a, v| not ovh.has_key?(a) }
293
+ logger.debug { "#{qp} has snapshot #{attr} value #{value.qp} not found in current content." }
294
+ false
295
+ else
296
+ vh.all? do |attr, value|
297
+ oval = ovh[attr]
298
+ eq = Resource.value_equal?(oval, value)
299
+ unless eq then
300
+ logger.debug { "#{qp} #{attr} snapshot value #{value.qp} differs from the current value #{oval.qp}." }
301
+ end
302
+ eq
303
+ end
304
+ end
305
+ end
306
+
307
+ # Adds this Persistable lazy loader to the given attribute unless the attribute already holds a fetched reference.
308
+ # Returns the loader if the loader was added to attribute.
309
+ def persistable__add_loader(attribute)
310
+ # bail if there is already a fetched reference
311
+ return if send(attribute).to_enum.any? { |ref| ref.identifier }
312
+
313
+ # the accessor methods to modify
314
+ attr_md = self.class.attribute_metadata(attribute)
315
+ reader, writer = attr_md.accessors
316
+ raise NotImplementedError.new("Missing writer method for #{self.class.qp} attribute #{attribute}") if writer.nil?
317
+
318
+ # define the singleton attribute reader method
319
+ instance_eval "def #{reader}; @lazy_loader ? persistable__load_reference(:#{attribute}) : super; end"
320
+ # define the singleton attribute writer method
321
+ instance_eval "def #{writer}(value); remove_lazy_loader(:#{attribute}); super; end"
322
+
323
+ @lazy_loader
324
+ end
325
+
326
+ # Loads the reference attribute database value into this Persistable.
327
+ #
328
+ # @param [Symbol] attribute the attribute to load
329
+ # @return the attribute value merged from the database value
330
+ def persistable__load_reference(attribute)
331
+ attr_md = self.class.attribute_metadata(attribute)
332
+ # bypass the singleton method and call the class instance method if the lazy loader is disabled
333
+ unless @lazy_loader.enabled? then
334
+ # the modified accessor method
335
+ reader, writer = attr_md.accessors
336
+ return self.class.instance_method(reader).bind(self).call
337
+ end
338
+
339
+ # Disable lazy loading first for the attribute, since the reader method might be called in
340
+ # the sequel, resulting in an infinite loop when the lazy loader is retriggered.
341
+ remove_lazy_loader(attribute)
342
+ logger.debug { "Lazy-loading #{qp} #{attribute}..." }
343
+ # the current value
344
+ oldval = send(attribute)
345
+ # load the fetched value
346
+ fetched = @lazy_loader.load(attribute)
347
+ # nothing to do if nothing fetched
348
+ return oldval if fetched.nil_or_empty?
349
+
350
+ # merge the fetched into the attribute
351
+ logger.debug { "Merging #{qp} fetched #{attribute} value #{fetched.qp}#{' into ' + oldval.qp if oldval}..." }
352
+ matcher = @lazy_loader.matcher
353
+ merged = merge_attribute_value(attribute, oldval, fetched, &matcher)
354
+ # update the snapshot of dependents
355
+ if attr_md.dependent? then
356
+ # the owner attribute
357
+ oattr = attr_md.inverse
358
+ if oattr then
359
+ # update dependent snapshot with the owner, since the owner snapshot is taken when fetched but the
360
+ # owner might be set when the fetched dependent is merged into the owner dependent attribute.
361
+ merged.enumerate do |dep|
362
+ if dep.snapshot_taken? then
363
+ dep.snapshot[oattr] = self
364
+ logger.debug { "Updated #{qp} #{attribute} fetched dependent #{dep.qp} snapshot with #{oattr} value #{qp}." }
365
+ end
366
+ end
367
+ end
368
+ end
369
+ merged
370
+ end
371
+
372
+ # Disables the given singleton attribute accessor method.
373
+ #
374
+ # @param [String, Symbol] name_or_sym the accessor method to disable
375
+ def disable_singleton_method(name_or_sym)
376
+ return unless singleton_methods.include?(name_or_sym.to_s)
377
+ # dissociate the method from this instance
378
+ method = self.method(name_or_sym.to_sym)
379
+ method.unbind
380
+ # JRuby alert - Unbind doesn't work in JRuby 1.1.6. In that case, redefine the singleton method to delegate
381
+ # to the class instance method.
382
+ if singleton_methods.include?(name_or_sym.to_s) then
383
+ args = (1..method.arity).map { |argnum| "arg#{argnum}" }.join(', ')
384
+ instance_eval "def #{name_or_sym}(#{args}); super; end"
385
+ end
386
+ end
387
+
388
+ class LazyLoader
389
+ # @return [Proc] the source => target matcher
390
+ attr_reader :matcher
391
+
392
+ # Creates a new LazyLoader which calls the loader Proc on the subject.
393
+ #
394
+ # @raise [ArgumentError] if the loader is not given to this initializer
395
+ def initialize(subject, loader=nil, &matcher)
396
+ @subject = subject
397
+ # the loader proc from either the argument or the block
398
+ @loader = loader
399
+ @matcher = matcher
400
+ raise ArgumentError.new("Neither a loader nor a block is given to the LazyLoader initializer") if @loader.nil?
401
+ @enabled = true
402
+ end
403
+
404
+ # Returns whether this loader is enabled.
405
+ def enabled?
406
+ @enabled
407
+ end
408
+
409
+ # Returns whether this loader is disabled.
410
+ def disabled?
411
+ not @enabled
412
+ end
413
+
414
+ # Disable this loader.
415
+ def disable
416
+ @enabled = false
417
+ end
418
+
419
+ # Enables this loader.
420
+ def enable
421
+ @enabled = true
422
+ end
423
+
424
+ # Returns the attribute value loaded from the database.
425
+ # Raises DatabaseError if this loader is disabled.
426
+ def load(attribute)
427
+ raise DatabaseError.new("#{qp} lazy load called on disabled loader") unless enabled?
428
+ @loader.call(@subject, attribute)
429
+ end
430
+ end
431
+ end
432
+ end