caruby-core 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/History.txt +10 -0
  2. data/lib/caruby/cli/command.rb +10 -8
  3. data/lib/caruby/database/fetched_matcher.rb +28 -39
  4. data/lib/caruby/database/lazy_loader.rb +101 -0
  5. data/lib/caruby/database/persistable.rb +190 -167
  6. data/lib/caruby/database/persistence_service.rb +21 -7
  7. data/lib/caruby/database/persistifier.rb +185 -0
  8. data/lib/caruby/database/reader.rb +106 -176
  9. data/lib/caruby/database/saved_matcher.rb +56 -0
  10. data/lib/caruby/database/search_template_builder.rb +1 -1
  11. data/lib/caruby/database/sql_executor.rb +8 -7
  12. data/lib/caruby/database/store_template_builder.rb +134 -61
  13. data/lib/caruby/database/writer.rb +252 -52
  14. data/lib/caruby/database.rb +88 -67
  15. data/lib/caruby/domain/attribute_initializer.rb +16 -0
  16. data/lib/caruby/domain/attribute_metadata.rb +161 -72
  17. data/lib/caruby/domain/id_alias.rb +22 -0
  18. data/lib/caruby/domain/inversible.rb +91 -0
  19. data/lib/caruby/domain/merge.rb +116 -35
  20. data/lib/caruby/domain/properties.rb +1 -1
  21. data/lib/caruby/domain/reference_visitor.rb +207 -71
  22. data/lib/caruby/domain/resource_attributes.rb +93 -80
  23. data/lib/caruby/domain/resource_dependency.rb +22 -97
  24. data/lib/caruby/domain/resource_introspection.rb +21 -28
  25. data/lib/caruby/domain/resource_inverse.rb +134 -0
  26. data/lib/caruby/domain/resource_metadata.rb +41 -19
  27. data/lib/caruby/domain/resource_module.rb +42 -33
  28. data/lib/caruby/import/java.rb +8 -9
  29. data/lib/caruby/migration/migrator.rb +20 -7
  30. data/lib/caruby/migration/resource_module.rb +0 -2
  31. data/lib/caruby/resource.rb +132 -351
  32. data/lib/caruby/util/cache.rb +4 -1
  33. data/lib/caruby/util/class.rb +48 -1
  34. data/lib/caruby/util/collection.rb +54 -18
  35. data/lib/caruby/util/inflector.rb +7 -0
  36. data/lib/caruby/util/options.rb +35 -31
  37. data/lib/caruby/util/partial_order.rb +1 -1
  38. data/lib/caruby/util/properties.rb +2 -2
  39. data/lib/caruby/util/stopwatch.rb +16 -8
  40. data/lib/caruby/util/transitive_closure.rb +1 -1
  41. data/lib/caruby/util/visitor.rb +342 -328
  42. data/lib/caruby/version.rb +1 -1
  43. data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
  44. data/lib/caruby.rb +2 -0
  45. metadata +10 -9
  46. data/lib/caruby/database/saved_merger.rb +0 -131
  47. data/lib/caruby/domain/annotatable.rb +0 -25
  48. data/lib/caruby/domain/annotation.rb +0 -23
  49. data/lib/caruby/import/annotatable_class.rb +0 -28
  50. data/lib/caruby/import/annotation_class.rb +0 -27
  51. data/lib/caruby/import/annotation_module.rb +0 -67
  52. data/lib/caruby/migration/resource.rb +0 -8
@@ -11,9 +11,7 @@ require 'caruby/util/class'
11
11
  require 'caruby/util/inflector'
12
12
  require 'caruby/util/collection'
13
13
 
14
- # include some standard Java classes
15
14
  module Java
16
-
17
15
  # Adds the directories in the given path and all Java jar files contained in the directories
18
16
  # to the execution classpath.
19
17
  #
@@ -165,13 +163,13 @@ module Java
165
163
  def to_ruby_date
166
164
  calendar = java.util.Calendar.instance
167
165
  calendar.setTime(self)
168
- secs = calendar.timeInMillis / 1000
166
+ secs = calendar.timeInMillis.to_f / 1000
169
167
  # millis since epoch
170
168
  time = Time.at(secs)
171
169
  # convert UTC timezone millisecond offset to Rational fraction of a day
172
170
  offset_millis = calendar.timeZone.getOffset(calendar.timeInMillis).to_f
173
171
  # adjust for DST
174
- if calendar.timeZone.useDaylightTime and not time.isdst then
172
+ if calendar.timeZone.useDaylightTime and time.isdst then
175
173
  offset_millis -= MILLIS_PER_HR
176
174
  end
177
175
  offset_days = offset_millis / MILLIS_PER_DAY
@@ -200,7 +198,7 @@ module Java
200
198
  # the Java date factory
201
199
  calendar = java.util.Calendar.instance
202
200
  # adjust for DST
203
- if calendar.timeZone.useDaylightTime and not Time.at(time).isdst then
201
+ if calendar.timeZone.useDaylightTime and Time.at(time).isdst then
204
202
  millis += MILLIS_PER_HR
205
203
  end
206
204
  calendar.setTimeInMillis(millis)
@@ -234,13 +232,13 @@ class Class
234
232
  # Otherwise, this method returns the Ruby class for the name of the presumed Java klass.
235
233
  def self.to_ruby(klass)
236
234
  case klass
237
- when Class then klass
238
- when String then Java.module_eval(klass)
239
- else to_ruby(klass.name)
235
+ when Class then klass
236
+ when String then Java.module_eval(klass)
237
+ else to_ruby(klass.name)
240
238
  end
241
239
  end
242
240
 
243
- # Returns whether this class is abstract.
241
+ # @return [Boolean] whether this is a wrapper for an abstract Java class
244
242
  def abstract?
245
243
  java_class? and Java::JavaLangReflect::Modifier.isAbstract(java_class.modifiers)
246
244
  end
@@ -260,6 +258,7 @@ class Class
260
258
  # If the hierarchy flag is set to +false+, then only this class's properties
261
259
  # will be introspected.
262
260
  def java_properties(hierarchy=true)
261
+ return Array::EMPTY_ARRAY unless java_class?
263
262
  info = hierarchy ? Java::JavaBeans::Introspector.getBeanInfo(java_class) : Java::JavaBeans::Introspector.getBeanInfo(java_class, java_class.superclass)
264
263
  info.propertyDescriptors.select { |pd| pd.write_method and property_read_method(pd) }
265
264
  end
@@ -1,7 +1,7 @@
1
1
  # load the required gems
2
2
  require 'rubygems'
3
3
 
4
- # the UOM gem
4
+ # the Units of Measurement gem
5
5
  gem 'uom'
6
6
 
7
7
  require 'enumerator'
@@ -15,7 +15,7 @@ require 'caruby/util/options'
15
15
  require 'caruby/util/pretty_print'
16
16
  require 'caruby/util/properties'
17
17
  require 'caruby/util/collection'
18
- require 'caruby/migration/resource'
18
+ require 'caruby/migration/migratable'
19
19
 
20
20
  module CaRuby
21
21
  class MigrationError < RuntimeError; end
@@ -33,7 +33,8 @@ module CaRuby
33
33
  # @option opts [String] :input required source file to migrate
34
34
  # @option opts [String] :shims optional array of shim files to load
35
35
  # @option opts [String] :bad optional invalid record file
36
- # @option opts [String] :offset zero-based starting source record number to process (default 0)
36
+ # @option opts [Integer] :offset zero-based starting source record number to process (default 0)
37
+ # @option opts [Boolean] :quiet suppress output messages
37
38
  def initialize(opts)
38
39
  parse_options(opts)
39
40
  build
@@ -72,6 +73,7 @@ module CaRuby
72
73
  migrate do |target|
73
74
  save(target, db)
74
75
  yield target if block_given?
76
+ db.clear
75
77
  end
76
78
  end
77
79
  end
@@ -97,12 +99,14 @@ module CaRuby
97
99
  @offset = opts[:offset] ||= 0
98
100
  @input = Options.get(:input, opts)
99
101
  raise MigrationError.new("Migrator missing required source file parameter") if @input.nil?
100
- @database = Options.get(:database, opts)
102
+ @database = opts[:database]
101
103
  raise MigrationError.new("Migrator missing required database parameter") if @database.nil?
102
- @target_class = Options.get(:target, opts)
104
+ @target_class = opts[:target]
103
105
  raise MigrationError.new("Migrator missing required target class parameter") if @target_class.nil?
104
106
  @bad_rec_file = opts[:bad]
105
107
  logger.info("Migration options: #{opts.reject { |option, value| value.nil_or_empty? }.pp_s}.")
108
+ # flag indicating whether to print a progress monitor
109
+ @print_progress = !opts[:quiet]
106
110
  end
107
111
 
108
112
  def build
@@ -282,6 +286,7 @@ module CaRuby
282
286
  rec_cnt += 1 && next if rec_cnt < @offset
283
287
  begin
284
288
  # migrate the row
289
+ logger.debug { "Migrating record #{rec_no}..." }
285
290
  target = migrate_row(row)
286
291
  # call the block on the migrated target
287
292
  if target then
@@ -298,6 +303,7 @@ module CaRuby
298
303
  logger.debug { "Migrated record #{rec_no}." }
299
304
  #memory_usage = `ps -o rss= -p #{Process.pid}`.to_f / 1024 # in megabytes
300
305
  #logger.debug { "Migrated rec #{@rec_cnt}; memory usage: #{sprintf("%.1f", memory_usage)} MB." }
306
+ if @print_progress then print_progress(mgt_cnt) end
301
307
  mgt_cnt += 1
302
308
  # clear the migration state
303
309
  clear(target)
@@ -315,6 +321,13 @@ module CaRuby
315
321
  end
316
322
  logger.info("Migrated #{mgt_cnt} of #{rec_cnt} records.")
317
323
  end
324
+
325
+ # Prints a +\++ progress indicator to stdout.
326
+ #
327
+ # @param [Integer] count the progress step count
328
+ def print_progress(count)
329
+ if count % 72 == 0 then puts "+" else print "+" end
330
+ end
318
331
 
319
332
  # Clears references to objects allocated for migration of a single row into the given target.
320
333
  # This method does nothing. Subclasses can override.
@@ -334,10 +347,10 @@ module CaRuby
334
347
  migrated = @creatable_classes.map { |klass| create(klass, row, created) }
335
348
  # migrate each object from the input row
336
349
  created.each { |obj| obj.migrate(row, migrated) }
337
- # set the references
338
- migrated.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
339
350
  # remove invalid migrations
340
351
  migrated.delete_if { |obj| not migration_valid?(obj) }
352
+ # set the references
353
+ migrated.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
341
354
  # the target object
342
355
  target = migrated.detect { |obj| @target_class === obj }
343
356
  if target then
@@ -1,5 +1,3 @@
1
- require 'caruby/domain/resource_module'
2
-
3
1
  module CaRuby
4
2
  module ResourceModule
5
3
  # Declares the given classes which will be dynamically modified for migration.
@@ -7,40 +7,17 @@ require 'caruby/util/collection'
7
7
  require 'caruby/domain/merge'
8
8
  require 'caruby/domain/reference_visitor'
9
9
  require 'caruby/database/persistable'
10
+ require 'caruby/domain/inversible'
10
11
  require 'caruby/domain/resource_metadata'
11
12
  require 'caruby/domain/resource_module'
13
+ require 'caruby/migration/migratable'
12
14
 
13
15
  module CaRuby
14
16
  # The Domain module is included by Java domain classes.
15
17
  # This module defines essential common domain methods that enable the jRuby-Java API bridge.
16
18
  # Classes which include Domain must implement the +metadata+ Domain::Metadata accessor method.
17
19
  module Resource
18
- include Comparable, Mergeable, Persistable, Validation
19
-
20
- # @param [{Symbol => Object}] the optional attribute => value hash
21
- # @return a new instance of this Resource class initialized from the optional attribute => value hash
22
- def initialize(hash=nil)
23
- super()
24
- if hash then
25
- unless Hashable === hash then
26
- raise ArgumentError.new("#{qp} initializer argument type not supported: #{hash.class.qp}")
27
- end
28
- merge_attributes(hash)
29
- end
30
- end
31
-
32
- # Returns the database identifier.
33
- # This method aliases a Java +id+ property accessor, since +id+ is a reserved Ruby attribute.
34
- def identifier
35
- getId
36
- end
37
-
38
- # Sets the database identifier to the given id value.
39
- # This method aliases the caTissue Java +id+ property setter,
40
- # since +id+ is a reserved Ruby attribute.
41
- def identifier=(id)
42
- setId(id)
43
- end
20
+ include Mergeable, Migratable, Persistable, Inversible, Validation
44
21
 
45
22
  # Sets the default attribute values for this domain object and its dependents. If this Resource
46
23
  # does not have an identifier, then missing attributes are set to the values defined by
@@ -52,7 +29,7 @@ module CaRuby
52
29
  # @return [Resource] self
53
30
  def add_defaults
54
31
  # apply owner defaults
55
- if owner then
32
+ if owner and owner.identifier.nil? then
56
33
  owner.add_defaults
57
34
  else
58
35
  logger.debug { "Adding defaults to #{qp} and its dependents..." }
@@ -71,7 +48,7 @@ module CaRuby
71
48
  # @raise [ValidationError] if a mandatory attribute value is missing
72
49
  def validate
73
50
  unless @validated then
74
- logger.debug { "Validating #{qp} required attributes #{self.class.mandatory_attributes.to_a.to_series}..." }
51
+ logger.debug { "Validating #{qp} required attributes #{self.mandatory_attributes.to_a.to_series}..." }
75
52
  invalid = missing_mandatory_attributes
76
53
  unless invalid.empty? then
77
54
  logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
@@ -124,61 +101,10 @@ module CaRuby
124
101
  self.class.new.merge_attributes(self, attributes)
125
102
  end
126
103
 
127
- # Merges the other into this domain object. The non-domain attributes are always merged.
128
- #
129
- # @param [Resource] other the merge source domain object
130
- # @param [<Symbol>] attributes the domain attributes to merge
131
- # @yield [source, target] mergethe source into the target
132
- # @yieldparam [Resource] source the reference to merge from
133
- # @yieldparam [Resource] target the reference the source is merge into
134
- # @raise [ArgumentError] if other is not an instance of this domain object's class
135
- # @see #merge_attribute_value
136
- def merge_match(other, attributes, &matcher)
137
- raise ArgumentError.new("Incompatible #{qp} merge source: #{other.qp}") unless self.class === other
138
- logger.debug { format_merge_log_message(other, attributes) }
139
- # merge the non-domain attributes
140
- merge_attributes(other)
141
- # merge the domain attributes
142
- unless attributes.nil_or_empty? then
143
- merge_attributes(other, attributes) do |attr, oldval, newval|
144
- merge_attribute_value(attr, oldval, newval, &matcher)
145
- end
146
- end
147
- end
148
-
149
- # Merges the attribute newval into oldval as follows:
150
- # * If attribute is a non-domain attribute and oldval is either nil or the attribute is not saved,
151
- # then the attribute value is set to newval.
152
- # * Otherwise, if attribute is a domain non-collection attribute, then newval is recursively
153
- # merged into oldval.
154
- # * Otherwise, if attribute is a domain collection attribute, then matching newval members are
155
- # merged into the corresponding oldval member and non-matching newval members are added to
156
- # newval.
157
- # * Otherwise, the attribute value is not changed.
158
- #
159
- # The domain value match is performed by the matcher block. The block arguments are newval and
160
- # oldval and the return value is a source => target hash of matching references. The default
161
- # matcher is {Resource#match_in}.
162
- #
163
- # @param [Symbol] attribute the merge attribute
164
- # @param oldval the current value
165
- # @param newval the value to merge
166
- # @yield [newval, oldval] the value matcher used if attribute is a domain collection
167
- # @yieldparam newval the merge source value
168
- # @yieldparam oldval this domain object's current attribute value
169
- # @return the merged attribute value
170
- def merge_attribute_value(attribute, oldval, newval, &matcher)
171
- attr_md = self.class.attribute_metadata(attribute)
172
- if attr_md.nondomain? then
173
- if oldval.nil? and not newval.nil? then
174
- send(attr_md.writer, newval)
175
- newval
176
- end
177
- else
178
- merge_domain_attribute_value(attr_md, oldval, newval, &matcher)
179
- end
180
- end
181
-
104
+ # Clears the given attribute value. If the current value responds to the +clear+ method,
105
+ # then the current value is cleared. Otherwise, the value is set to {ResourceMetadata#empty_value}.
106
+ #
107
+ # @param [Symbol] attribute the attribute to clear
182
108
  def clear_attribute(attribute)
183
109
  # the current value to clear
184
110
  current = send(attribute)
@@ -188,7 +114,9 @@ module CaRuby
188
114
  if current.respond_to?(:clear) then
189
115
  current.clear
190
116
  else
191
- send(self.class.attribute_metadata(attribute).writer, empty_value(attribute))
117
+ writer = self.class.attribute_metadata(attribute).writer
118
+ value = self.class.empty_value(attribute)
119
+ send(writer, value)
192
120
  end
193
121
  end
194
122
 
@@ -197,7 +125,7 @@ module CaRuby
197
125
  # is preserved, e.g. an Array value is assigned to a set domain type by first clearing the set
198
126
  # and then merging the array content into the set.
199
127
  #
200
- # @see #merge_attribute
128
+ # @see Mergeable#merge_attribute
201
129
  def set_attribute(attribute, value)
202
130
  # bail out if the value argument is the current value
203
131
  return value if value.equal?(send(attribute))
@@ -207,26 +135,42 @@ module CaRuby
207
135
 
208
136
  # Returns the secondary key attribute values as follows:
209
137
  # * If there is no secondary key, then this method returns nil.
210
- # * Otherwise, if the secondary key attributes is a singleton Array, then the key is the value of the sole key attribute.
138
+ # * Otherwise, if the secondary key attributes is a singleton Array, then the key is the
139
+ # value of the sole key attribute.
211
140
  # * Otherwise, the key is an Array of the key attribute values.
141
+ #
142
+ # @return [Array, Object] the key attribute values
212
143
  def key
213
144
  attrs = self.class.secondary_key_attributes
214
145
  case attrs.size
215
- when 0 then nil
216
- when 1 then send(attrs.first)
217
- else attrs.map { |attr| send(attr) }
146
+ when 0 then nil
147
+ when 1 then send(attrs.first)
148
+ else attrs.map { |attr| send(attr) }
218
149
  end
219
150
  end
220
151
 
221
- # Returns the domain object that owns this object, or nil if this object is not dependent on an owner.
152
+ # @return [Resource, nil] the domain object that owns this object, or nil if this object
153
+ # is not dependent on an owner
222
154
  def owner
223
155
  self.class.owner_attributes.detect_value { |attr| send(attr) }
224
156
  end
157
+
158
+ # Sets this dependent's owner attribute to the given domain object.
159
+ #
160
+ # @param [Resource] owner the owner domain object
161
+ # @raise [NoMethodError] if this Resource's class does not have exactly one owner attribute
162
+ def owner=(owner)
163
+ attr = self.class.owner_attribute
164
+ if attr.nil? then raise NoMethodError.new("#{self.class.qp} does not have a unique owner attribute") end
165
+ set_attribute(attr, owner)
166
+ end
225
167
 
226
- # Returns whether the other domain object is this object's #owner or an #owner_ancestor? of this object's {#owner}.
168
+ # @param [Resource] other the domain object to check
169
+ # @return [Boolean] whether the other domain object is this object's {#owner} or an
170
+ # {#owner_ancestor?} of this object's {#owner}
227
171
  def owner_ancestor?(other)
228
- ref = owner
229
- ref and (ref == other or ref.owner_ancestor?(other))
172
+ owner = self.owner
173
+ owner and (owner == other or owner.owner_ancestor?(other))
230
174
  end
231
175
 
232
176
  # Returns an attribute => value hash for the specified attributes with a non-nil, non-empty value.
@@ -249,12 +193,12 @@ module CaRuby
249
193
  attributes.map { |attr| send(attr) }.flatten.compact
250
194
  end
251
195
 
252
- # Returns whether this domain object is dependent on another entity.
196
+ # @return [Boolean] whether this domain object is dependent on another entity
253
197
  def dependent?
254
198
  self.class.dependent?
255
199
  end
256
200
 
257
- # Returns whether this domain object is not dependent on another entity.
201
+ # @return [Boolean] whether this domain object is not dependent on another entity
258
202
  def independent?
259
203
  not dependent?
260
204
  end
@@ -265,14 +209,23 @@ module CaRuby
265
209
  # @yieldparam [Resource] dep the dependent
266
210
  def each_dependent
267
211
  self.class.dependent_attributes.each do |attr|
268
- send(attr).enumerate { |dep| yield dep }
212
+ send(attr).enumerate { |dep| yield dep }
269
213
  end
270
214
  end
271
215
 
272
- # The dependent Enumerable.
216
+ # @return [Enumerable] this domain object's dependents
273
217
  def dependents
274
218
  enum_for(:each_dependent)
275
219
  end
220
+
221
+ # Returns the attributes which are required for save. This base implementation returns the
222
+ # class {ResourceAttributes#mandatory_attributes}. Subclasses can override this method
223
+ # for domain object state-specific refinements.
224
+ #
225
+ # @return [<Symbol>] the required attributes for a save operation
226
+ def mandatory_attributes
227
+ self.class.mandatory_attributes
228
+ end
276
229
 
277
230
  # Returns the attribute references which directly depend on this owner.
278
231
  # The default is the attribute value.
@@ -285,26 +238,21 @@ module CaRuby
285
238
  # dependency path, e.g. in caTissue a Specimen is owned by both a SCG and a parent
286
239
  # Specimen. In that case, the SCG direct dependents consist of top-level Specimens
287
240
  # owned by the SCG but not derived from another Specimen.
241
+ #
242
+ # @param [Symbol] attribute the dependent attribute
243
+ # @return [<Resource>] the attribute value, wrapped in an array if necessary
288
244
  def direct_dependents(attribute)
289
245
  deps = send(attribute)
290
246
  case deps
291
- when Enumerable then deps
292
- when nil then Array::EMPTY_ARRAY
293
- else [deps]
247
+ when Enumerable then deps
248
+ when nil then Array::EMPTY_ARRAY
249
+ else [deps]
294
250
  end
295
251
  end
296
252
 
297
- # Returns pairs +[+_value1_, _value2_+]+ of correlated non-nil values for every attribute in attributes
298
- # in this and the other domain object, e.g. given domain objects +site1+ and +site2+ with non-nil
299
- # +address+ dependents:
300
- # site1.assoc_attribute_values(site2, [:address]) #=> [site1.address, site2.address]
301
- def assoc_attribute_values(other, attributes)
302
- return {} if other.nil? or attributes.empty?
303
- # associate the attribute => value hashes for this object and the other hash into one attribute => [value1, value2] hash
304
- value_hash(attributes).assoc_values(other.value_hash(attributes)).values.reject { |values| values.any? { |value| value.nil? } }
305
- end
306
-
307
- # Returns whether this object matches the fetched other object on class and key values.
253
+ # @param [Resource] the domain object to match
254
+ # @return [Boolean] whether this object matches the fetched other object on class
255
+ # and key values
308
256
  def match?(other)
309
257
  match_in([other])
310
258
  end
@@ -321,6 +269,9 @@ module CaRuby
321
269
  # This domain object is matched against the others on the above attributes in succession
322
270
  # until a unique match is found. The key attribute matches are strict, i.e. each
323
271
  # key attribute value must be non-nil and match the other value.
272
+ #
273
+ # @param [<Resource>] the candidate domain object matches
274
+ # @return [Resource, nil] the matching domain object, or nil if no match
324
275
  def match_in(others)
325
276
  # trivial case: self is in others
326
277
  return self if others.include?(self)
@@ -332,37 +283,20 @@ module CaRuby
332
283
  match_unique_object_with_attributes(others, self.class.alternate_key_attributes)
333
284
  end
334
285
 
335
- # Returns the match of this domain object in the scope of a matching owner, if any.
336
- # If this domain object is dependent, then the match is performed in the context of a
337
- # matching owner object. If {#match_in} returns a match, then that result is used.
338
- # Otherwise, #match_without_owner_attribute is called.
286
+ # Returns the match of this domain object in the scope of a matching owner as follows:
287
+ # * If {#match_in} returns a match, then that match is the result is used.
288
+ # * Otherwise, if this is a dependent attribute then the match is attempted on a
289
+ # secondary key without owner attributes. Defaults are added to this object in order
290
+ # to pick up potential secondary key values.
339
291
  #
340
- # @param [<Resource>] others the candidate domain objects for the match
341
- # @return [Resource, nil] the matching domain object, if any
292
+ # @param (see #match_in)
293
+ # @return (see #match_in)
342
294
  def match_in_owner_scope(others)
343
- match_in(others) or others.detect { |other| match_without_owner_attribute(other) }
295
+ match_in(others) or others.detect { |other| match_without_owner_attribute?(other) }
344
296
  end
345
297
 
346
- # Returns whether the other domain object matches this domain object on a secondary
347
- # key without owner attributes. Defaults are added to this object in order to pick up
348
- # potential secondary key values.
349
- #
350
- # @param [<Resource>] other the domain object to match against
351
- # @return [Boolean] whether the other domain object matches this domain object on a
352
- # secondary key without owner attributes
353
- def match_without_owner_attribute(other)
354
- oattrs = self.class.owner_attributes
355
- return if oattrs.empty?
356
- # add defaults to pick up potential secondary key value
357
- add_defaults_local
358
- # match on the secondary key
359
- self.class.secondary_key_attributes.all? do |attr|
360
- oattrs.include?(attr) or matches_attribute_value?(other, attr, send(attr))
361
- end
362
- end
363
-
364
- # @return [{Resouce => Resource}] a source => target hash of the given sources which match the
365
- # targets using the {#match_in} method
298
+ # @return [{Resouce => Resource}] a source => target hash of the given sources which match
299
+ # the targets using the {#match_in} method
366
300
  def self.match_all(sources, targets)
367
301
  DEF_MATCHER.match(sources, targets)
368
302
  end
@@ -465,6 +399,8 @@ module CaRuby
465
399
  yield(ref) and ref.visit_owners(&operator) if ref
466
400
  end
467
401
 
402
+ # @param q the PrettyPrint queue
403
+ # @return [String] the formatted content of this Resource
468
404
  def pretty_print(q)
469
405
  q.text(qp)
470
406
  content = printable_content
@@ -472,14 +408,16 @@ module CaRuby
472
408
  end
473
409
 
474
410
  # Prints this domain object's content and recursively prints the referenced content.
475
- # The optional selector block determines the attributes to print. The default is all
476
- # Java domain attributes.
411
+ # The optional selector block determines the attributes to print. The default is the
412
+ # {ResourceAttributes#java_attributes}. The database lazy loader is disabled during
413
+ # the execution of this method. Thus, the printed content reflects the transient
414
+ # in-memory object graph rather than the persistent content.
477
415
  #
478
416
  # @yield [owner] the owner attribute selector
479
417
  # @yieldparam [Resource] owner the domain object to print
480
418
  # @return [String] the domain object content
481
419
  def dump(&selector)
482
- DetailPrinter.new(self, &selector).pp_s
420
+ database.lazy_loader.disable { DetailPrinter.new(self, &selector).pp_s }
483
421
  end
484
422
 
485
423
  # Prints this domain object in the format:
@@ -508,27 +446,18 @@ module CaRuby
508
446
  # @return [{Symbol => String}] the attribute => content hash
509
447
  def printable_content(attributes=nil, &reference_printer) # :yields: reference
510
448
  attributes ||= printworthy_attributes
511
- vh = suspend_lazy_loader { value_hash(attributes) }
449
+ vh = value_hash(attributes)
512
450
  vh.transform { |value| printable_value(value, &reference_printer) }
513
451
  end
514
452
 
515
453
  # Returns whether value equals other modulo the given matches according to the following tests:
516
454
  # * _value_ == _other_
517
- # * _value_ and _other_ are Ruby DateTime instances and _value_ equals _other_,
518
- # modulo the Java-Ruby DST differences described in the following paragraph
519
- # * _value_ == matches[_other_]
520
455
  # * _value_ and _other_ are Resource instances and _value_ is a {#match?} with _other_.
521
456
  # * _value_ and _other_ are Enumerable with members equal according to the above conditions.
522
- # The DateTime comparison accounts for differences in the Ruby -> Java -> Ruby roundtrip
523
- # of a date attribute.
457
+ # * _value_ and _other_ are DateTime instances and are equal to within one second.
524
458
  #
525
- # Ruby alert - Unlike Java, Ruby does not adjust the offset for DST.
526
- # This adjustment is made in the {Java::JavaUtil::Date#to_ruby_date}
527
- # and {Java::JavaUtil::Date.from_ruby_date} methods.
528
- # A date sent to the database is stored correctly.
529
- # However, a date sent to the database does not correctly compare to
530
- # the data fetched from the database.
531
- # This method accounts for the DST discrepancy when comparing dates.
459
+ # The DateTime comparison accounts for differences in the Ruby -> Java -> Ruby roundtrip
460
+ # of a date attribute, which loses the seconds fraction.
532
461
  #
533
462
  # @return whether value and other are equal according to the above tests
534
463
  def self.value_equal?(value, other, matches=nil)
@@ -537,7 +466,7 @@ module CaRuby
537
466
  elsif value.collection? and other.collection? then
538
467
  collection_value_equal?(value, other, matches)
539
468
  elsif DateTime === value and DateTime === other then
540
- value == other or dates_equal_modulo_dst(value, other)
469
+ (value - other).abs.floor.zero?
541
470
  elsif Resource === value and value.class === other then
542
471
  value.match?(other)
543
472
  elsif matches then
@@ -552,7 +481,7 @@ module CaRuby
552
481
  # Adds the default values to this object, if it is not already fetched, and its dependents.
553
482
  def add_defaults_recursive
554
483
  # add the local defaults unless there is an identifier
555
- add_defaults_local unless identifier
484
+ add_defaults_local
556
485
  # add dependent defaults
557
486
  each_defaults_dependent { |dep| dep.add_defaults_recursive }
558
487
  end
@@ -563,7 +492,7 @@ module CaRuby
563
492
  # work around a caTissue bug (see that module for details). Other definitions
564
493
  # of this method are discouraged.
565
494
  def missing_mandatory_attributes
566
- self.class.mandatory_attributes.select { |attr| send(attr).nil_or_empty? }
495
+ mandatory_attributes.select { |attr| send(attr).nil_or_empty? }
567
496
  end
568
497
 
569
498
  private
@@ -603,8 +532,8 @@ module CaRuby
603
532
  # If a subclass overrides this method, then it should call super before setting the local
604
533
  # default attributes. This ensures that configuration defaults takes precedence.
605
534
  def add_defaults_local
535
+ logger.debug { "Adding defaults to #{qp}..." }
606
536
  merge_attributes(self.class.defaults)
607
- self
608
537
  end
609
538
 
610
539
  # Enumerates the dependents for setting defaults. Subclasses can override if the
@@ -616,46 +545,9 @@ module CaRuby
616
545
  not (attributes.empty? or attributes.any? { |attr| send(attr).nil? })
617
546
  end
618
547
 
619
- # Returns the source => target hash of matches for the given attr_md newval sources and
620
- # oldval targets. If the matcher block is given, then that block is called on the sources
621
- # and targets. Otherwise, {Resource.match_all} is called.
622
- #
623
- # @param [AttributeMetadata] attr_md the attribute to match
624
- # @param newval the source value
625
- # @param oldval the target value
626
- # @yield [sources, targets] matches sources to targets
627
- # @yieldparam [<Resource>] sources an Enumerable on the source value
628
- # @yieldparam [<Resource>] targets an Enumerable on the target value
629
- # @return [{Resource => Resource}] the source => target matches
630
- def match_attribute_value(attr_md, newval, oldval)
631
- # make Enumerable targets and sources for matching
632
- sources = newval.to_enum
633
- targets = oldval.to_enum
634
-
635
- # match sources to targets
636
- logger.debug { "Matching source #{newval.qp} to target #{qp} #{attr_md} #{oldval.qp}..." }
637
- block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
638
- end
639
-
640
- # @param [DateTime] d1 the first date
641
- # @param [DateTime] d2 the second date
642
- # @return whether d1 matches d2 on the non-offset fields
643
- # @see #value_equal
644
- def self.dates_equal_modulo_dst(d1, d2)
645
- d1.strftime('%FT%T') == d1.strftime('%FT%T')
646
- end
647
-
648
548
  def self.collection_value_equal?(value, other, matches=nil)
649
549
  value.size == other.size and value.all? { |v| other.include?(v) or (matches and other.include?(matches[v])) }
650
550
  end
651
-
652
- # @param (see #merge_match)
653
- # @return [String] the log message
654
- def format_merge_log_message(other, attributes)
655
- attr_list = attributes.sort { |a1, a2| a1.to_s <=> a2.to_s }
656
- attr_clause = " including domain attributes #{attr_list.to_series}" unless attr_list.empty?
657
- "Merging #{other.qp} into #{qp}#{attr_clause}..."
658
- end
659
551
 
660
552
  # A DetailPrinter formats a domain object value for printing using {#to_s} the first time the object
661
553
  # is encountered and a ReferencePrinter on the object subsequently.
@@ -749,68 +641,19 @@ module CaRuby
749
641
  end
750
642
  end
751
643
 
752
- # @see #merge_attribute_value
753
- def merge_domain_attribute_value(attr_md, oldval, newval, &matcher)
754
- # the source => target matches
755
- matches = match_attribute_value(attr_md, newval, oldval, &matcher)
756
- logger.debug { "Matched #{qp} #{attr_md}: #{matches.qp}." } unless matches.empty?
757
- merge_matching_attribute_references(attr_md, oldval, newval, matches)
758
- # return the merged result
759
- send(attr_md.reader)
760
- end
761
-
762
- # @see #merge_attribute_value
763
- def merge_matching_attribute_references(attr_md, oldval, newval, matches)
764
- # the dependent owner writer method, if any
765
- inv_md = attr_md.inverse_attribute_metadata
766
- if inv_md and not inv_md.collection? then
767
- owtr = inv_md.writer
768
- end
769
-
770
- # if the attribute is a collection, then merge the matches into the current attribute
771
- # collection value and add each unmatched source to the collection.
772
- # otherwise, if the attribute is not yet set and there is a new value, then set it
773
- # to the new value match or the new value itself if unmatched.
774
- if attr_md.collection? then
775
- # TODO - refactor into method
776
- # the references to add
777
- adds = []
778
- logger.debug { "Merging #{newval.qp} into #{qp} #{attr_md} #{oldval.qp}..." } unless newval.empty?
779
- newval.each do |src|
780
- # if the match target is in the current collection, then update the matched
781
- # target from the source.
782
- # otherwise, if there is no match or the match is a new reference created
783
- # from the match, then add the match to the oldval collection.
784
- if matches.has_key?(src) then
785
- # the source match
786
- tgt = matches[src]
787
- if oldval.include?(tgt) then
788
- tgt.merge_attributes(src)
789
- else
790
- adds << tgt
791
- end
792
- else
793
- adds << src
794
- end
795
- end
796
- # add the unmatched sources
797
- logger.debug { "Adding #{qp} #{attr_md} unmatched #{adds.qp}..." } unless adds.empty?
798
- adds.each do |ref|
799
- # if there is an owner writer attribute, then add the ref to the attribute collection by
800
- # delegating to the owner writer. otherwise, add the ref to the attribute collection directly.
801
- owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : oldval << ref
802
- end
803
- elsif newval then
804
- if oldval then
805
- oldval.merge(newval)
806
- else
807
- # the target is either a new object created in the match or the source value
808
- ref = matches.has_key?(newval) ? matches[newval] : newval
809
- # if the target is a dependent, then set the dependent owner, which in turn will
810
- # set the attribute to the dependent. otherwise, set attribute to the target.
811
- logger.debug { "Setting #{qp} #{attr_md} to #{ref.qp}..." }
812
- owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : send(attr_md.writer, ref)
813
- end
644
+ # Returns whether the other domain object matches this domain object on a secondary
645
+ # key without owner attributes. Defaults are added to this object in order to pick up
646
+ # potential secondary key values.
647
+ #
648
+ # @param (see #match_in)
649
+ # @return [Boolean] whether the other domain object matches this domain object on a
650
+ # secondary key without owner attributes
651
+ def match_without_owner_attribute?(other)
652
+ oattrs = self.class.owner_attributes
653
+ return if oattrs.empty?
654
+ # match on the secondary key
655
+ self.class.secondary_key_attributes.all? do |attr|
656
+ oattrs.include?(attr) or matches_attribute_value?(other, attr, send(attr))
814
657
  end
815
658
  end
816
659
 
@@ -819,102 +662,17 @@ module CaRuby
819
662
  ref.send(writer, self)
820
663
  end
821
664
 
822
- # Sets an exclusive dependent attribute to the given dependent dep.
823
- # If dep is not nil, then this method calls the dep inv_writer with argument self
824
- # before calling the writer with argument dep.
825
- def set_exclusive_dependent(dep, writer, inv_writer)
826
- dep.send(inv_writer, self) if dep
827
- send(writer, dep)
828
- end
829
-
830
- # Sets the inversible attribute with the given accessors and inverse to
831
- # the given newval. The inverse of the attribute is a collection accessed by
832
- # calling inverse on newval.
833
- #
834
- # For an attribute +owner+ with writer +setOwner+ and inverse reader +deps+,
835
- # this is equivalent to the following:
836
- # class Dependent
837
- # def owner=(o)
838
- # owner.deps.delete(owner) if owner
839
- # setOwner(o)
840
- # o.deps << self if o
841
- # end
842
- # end
843
- #
844
- # @param [Resource] new_ref the new attribute reference value
845
- # @param [(Symbol, Symbol)] accessors the reader and writer to use in setting
846
- # the attribute
847
- # @param [Symbol] inverse the inverse collection attribute to which
848
- # this domain object will be added
849
- def add_to_inverse_collection(new_ref, accessors, inverse)
850
- reader, writer = accessors
851
- # the current inverse
852
- old_ref = send(reader)
853
- # no-op if no change
854
- return new_ref if old_ref == new_ref
855
-
856
- # delete self from the current inverse reference collection
857
- if old_ref then
858
- old_ref.suspend_lazy_loader do
859
- oldcoll = old_ref.send(inverse)
860
- oldcoll.delete(self) if oldcoll
861
- end
862
- end
863
-
864
- # call the writer on this object
865
- send(writer, new_ref)
866
- # add self to the inverse collection
867
- if new_ref then
868
- new_ref.suspend_lazy_loader do
869
- newcoll = new_ref.send(inverse)
870
- newcoll << self
871
- unless newcoll then
872
- raise TypeError.new("Cannot create #{new_ref.qp} #{inverse} collection to hold #{self}")
873
- end
874
- if old_ref then
875
- logger.debug { "Moved #{qp} from #{reader} #{old_ref.qp} #{inverse} to #{new_ref.qp}." }
876
- else
877
- logger.debug { "Added #{qp} to #{reader} #{new_ref.qp} #{inverse}." }
878
- end
879
- end
880
- end
881
- new_ref
882
- end
883
-
884
- # Sets the attribute with the given accessors and inverse_writer to the given newval.
885
- #
886
- # For an attribute +owner+ with writer +setOwner+ and inverse +dep+, this is equivalent
887
- # to the following:
888
- # class Dependent
889
- # def owner=(o)
890
- # owner.dep = nil if owner
891
- # setOwner(o)
892
- # o.dep = self if o
893
- # end
894
- # end
895
- def set_inversible_noncollection_attribute(newval, accessors, inverse_writer)
896
- reader, writer = accessors
897
- # the previous value
898
- oldval = suspend_lazy_loader { send(reader) }
899
- # bail if no change
900
- return newval if newval.equal?(oldval)
901
-
902
- # clear the previous inverse
903
- oldval.send(inverse_writer, nil) if oldval
904
- # call the writer
905
- send(writer, newval)
906
- logger.debug { "Moved #{qp} from #{oldval.qp} to #{newval.qp}." } if oldval and newval
907
- # call the inverse writer on self
908
- newval.send(inverse_writer, self) if newval
909
- newval
910
- end
911
-
912
665
  # Returns 0 if attribute is a Java primitive number,
913
- # +false+ if attribute is a Java primitive boolean, nil otherwise.
666
+ # +false+ if attribute is a Java primitive boolean,
667
+ # an empty collectin if the Java property is a collection,
668
+ # nil otherwise.
914
669
  def empty_value(attribute)
915
- type = java_type(attribute)
916
- return unless type and type.primitive?
917
- type.name == 'boolean' ? false : 0
670
+ type = java_type(attribute) || return
671
+ if type.primitive? then
672
+ type.name == 'boolean' ? false : 0
673
+ else
674
+ self.class.empty_value(attribute)
675
+ end
918
676
  end
919
677
 
920
678
  # Returns the Java type of the given attribute, or nil if attribute is not a Java property attribute.
@@ -923,6 +681,29 @@ module CaRuby
923
681
  attr_md.property_descriptor.property_type if JavaAttributeMetadata === attr_md
924
682
  end
925
683
 
684
+ # Returns the source => target hash of matches for the given attr_md newval sources and
685
+ # oldval targets. If the matcher block is given, then that block is called on the sources
686
+ # and targets. Otherwise, {Resource.match_all} is called.
687
+ #
688
+ # @param [AttributeMetadata] attr_md the attribute to match
689
+ # @param newval the source value
690
+ # @param oldval the target value
691
+ # @yield [sources, targets] matches sources to targets
692
+ # @yieldparam [<Resource>] sources an Enumerable on the source value
693
+ # @yieldparam [<Resource>] targets an Enumerable on the target value
694
+ # @return [{Resource => Resource}] the source => target matches
695
+ def match_attribute_value(attr_md, newval, oldval)
696
+ # make Enumerable targets and sources for matching
697
+ sources = newval.to_enum
698
+ targets = oldval.to_enum
699
+
700
+ # match sources to targets
701
+ logger.debug { "Matching source #{newval.qp} to target #{qp} #{attr_md} #{oldval.qp}..." } unless oldval.nil_or_empty?
702
+ matches = block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
703
+ logger.debug { "Matched #{qp} #{attr_md}: #{matches.qp}." } unless matches.empty?
704
+ matches
705
+ end
706
+
926
707
  # Returns the object in others which uniquely matches this domain object on the given attributes,
927
708
  # or nil if there is no unique match. This method returns nil if any attributes value is nil.
928
709
  def match_unique_object_with_attributes(others, attributes)