caruby-core 1.4.2 → 1.4.3

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