caruby-core 1.4.1

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.
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