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,338 @@
1
+ #
2
+ # Include file to set up the classpath and logger.
3
+ #
4
+
5
+ # The jRuby Java bridge
6
+ require 'java'
7
+ require 'ftools'
8
+ require 'date'
9
+
10
+ require 'caruby/util/class'
11
+ require 'caruby/util/inflector'
12
+ require 'caruby/util/collection'
13
+
14
+ # include some standard Java classes
15
+ module Java
16
+
17
+ # Adds the directories in the given path and all Java jar files contained in the directories
18
+ # to the execution classpath.
19
+ #
20
+ # @param path the colon or semi-colon separated directories
21
+ def self.add_path(path)
22
+ # the path directories
23
+ dirs = path.split(/[:;]/).map { |dir| File.expand_path(dir) }
24
+ # Add all jars found anywhere within the directories to the the classpath.
25
+ add_jars(*dirs)
26
+ # Add the directories to the the classpath.
27
+ dirs.each { |dir| $CLASSPATH << dir }
28
+ end
29
+
30
+ # Adds the jars in the directories to the execution class path.
31
+ #
32
+ # @param directories the directories containing jars to add
33
+ def self.add_jars(*directories)
34
+ directories.each do |dir|
35
+ Dir[File.join(dir , "**", "*.jar")].each { |jar| $CLASSPATH << jar }
36
+ end
37
+ end
38
+
39
+ module JavaUtil
40
+ # Aliases Java Collection methods with the standard Ruby Set counterpart, e.g. +delete+ for +remove+.
41
+ module Collection
42
+ def to_a
43
+ inject(Array.new) { |array, item| array << item }
44
+ end
45
+
46
+ # Removes the given item from this collection.
47
+ def delete(item)
48
+ # can't alias delete to remove, since a Java interface doesn't implement any methods
49
+ remove(item)
50
+ end
51
+
52
+ # Removes the items from this collection for which the block given to this method returns a non-nil, non-false value.
53
+ def delete_if
54
+ removeAll(select { |item| yield item })
55
+ self
56
+ end
57
+ end
58
+
59
+ # Aliases Java List methods with the standard Ruby Array counterpart, e.g. +merge+ for +addAll+.
60
+ module List
61
+ # Returns whether this List has the same content as the other Java List or Ruby Array.
62
+ def ==(other)
63
+ Array === other ? to_a == other : equals(other)
64
+ end
65
+
66
+ # Removes the given item from this collection.
67
+ def delete(item)
68
+ remove(item)
69
+ end
70
+ end
71
+
72
+ module Map
73
+ # Returns whether this Set has the same content as the other Java Map or Ruby Hash.
74
+ def ==(other)
75
+ ::Hash === other ? (size == other.size and other.all? { |key, value| get(key) == value }) : equals(other)
76
+ end
77
+
78
+ # Merges the other Java Map or Ruby Hash into this Map. Returns this modified Map.
79
+ #
80
+ # If a block is given to this method, then the block determines the mapped value
81
+ # as specified in the Ruby Hash merge method documentation.
82
+ def merge(other)
83
+ other.each do |key, value|
84
+ value = yield(key, get(key), value) if block_given? and containsKey(key)
85
+ put(key, value)
86
+ end
87
+ self
88
+ end
89
+
90
+ alias :merge! :merge
91
+ end
92
+
93
+ module Set
94
+ # Returns whether this Set has the same content as the other Java Set or Ruby Set.
95
+ def ==(other)
96
+ ::Set === other ? (size == other.size and all? { |item| other.include?(item) }) : equals(other)
97
+ end
98
+
99
+ # Merges the other Enumerable into this Set. Returns this modified Set.
100
+ #
101
+ # This method conforms to the Ruby Set merge contract rather than the Ruby List and Hash
102
+ # merge contract. Ruby Set merge modifies the Set in-place, whereas Ruby List and Hash
103
+ # merge return a new collection.
104
+ def merge(other)
105
+ return self if other.nil?
106
+ raise ArgumentError.new("Merge argument must be enumerable: #{other}") unless Enumerable === other
107
+ other.each { |item| self << item }
108
+ self
109
+ end
110
+
111
+ alias :merge! :merge
112
+ end
113
+
114
+ class HashSet
115
+ alias :base__clear :clear
116
+ private :base__clear
117
+ def clear
118
+ base__clear
119
+ self
120
+ end
121
+ end
122
+
123
+ class TreeSet
124
+ alias :base__first :first
125
+ private :base__first
126
+ # Fixes the jRuby {TreeSet#first} to return nil on an empty set rather than raise a Java exception.
127
+ def first
128
+ empty? ? nil : base__first
129
+ end
130
+ end
131
+
132
+ class ArrayList
133
+ alias :base__clear :clear
134
+ private :base__clear
135
+ def clear
136
+ base__clear
137
+ self
138
+ end
139
+ end
140
+
141
+ class Date
142
+ # millisecond-to-day conversion factor
143
+ MILLIS_PER_HR = 60 * 60 * 1000
144
+ MILLIS_PER_DAY = MILLIS_PER_HR * 24
145
+
146
+ # Converts this Java Date to a Ruby DateTime.
147
+ #
148
+ # caTissue alert - Bug #165: API CPR create date validation is time zone dependent.
149
+ # Since Java Date accounts for DST and Ruby DateTime doesn't,
150
+ # this method makes the DST adjustment by subtracting a compensatory
151
+ # one-hour DST offset from the Java Date time zone offset and using
152
+ # that to set the DateTime offset. This ensures that Date
153
+ # conversion is idempotent, i.e.
154
+ # date.to_ruby_date().to_java_date == date
155
+ #
156
+ # However, there can be adverse consequences for an application that assumes
157
+ # that the client time zone is the same as the server time zone, as described
158
+ # in caTissue Bug #165.
159
+ #
160
+ # TODO: Revisit {CaRuby::Resource.value_equal?} which must resort to a
161
+ # date-as-string comparison, always a bad idea. If that can be fixed, then
162
+ # increment/decrement the hour field rather than the offset field.
163
+ #
164
+ # @return [DateTime] the Ruby date
165
+ def to_ruby_date
166
+ calendar = java.util.Calendar.instance
167
+ calendar.setTime(self)
168
+ secs = calendar.timeInMillis / 1000
169
+ # millis since epoch
170
+ time = Time.at(secs)
171
+ # convert UTC timezone millisecond offset to Rational fraction of a day
172
+ offset_millis = calendar.timeZone.getOffset(calendar.timeInMillis).to_f
173
+ # adjust for DST
174
+ if calendar.timeZone.useDaylightTime and not time.isdst then
175
+ offset_millis -= MILLIS_PER_HR
176
+ end
177
+ offset_days = offset_millis / MILLIS_PER_DAY
178
+ offset_fraction = 1 / offset_days
179
+ offset = Rational(1, offset_fraction)
180
+ # convert to DateTime
181
+ DateTime.civil(time.year, time.mon, time.day, time.hour, time.min, time.sec, offset)
182
+ end
183
+
184
+ # Converts a Ruby Date or DateTime to a Java Date.
185
+ #
186
+ # @param [::Date, DateTime] date the Ruby date
187
+ # @return [Date] the Java date
188
+ def self.from_ruby_date(date)
189
+ return if date.nil?
190
+ # DateTime has time attributes, Date doesn't
191
+ if DateTime === date then
192
+ hour, min, sec = date.hour, date.min, date.sec
193
+ else
194
+ hour = min = sec = 0
195
+ end
196
+ # the Ruby time
197
+ time = Time.mktime(date.year, date.mon, date.day, hour, min, sec)
198
+ # millis since epoch
199
+ millis = (time.to_f * 1000).truncate
200
+ # the Java date factory
201
+ calendar = java.util.Calendar.instance
202
+ # adjust for DST
203
+ if calendar.timeZone.useDaylightTime and not Time.at(time).isdst then
204
+ millis += MILLIS_PER_HR
205
+ end
206
+ calendar.setTimeInMillis(millis)
207
+ calendar.getTime
208
+ end
209
+ end
210
+ end
211
+
212
+ def self.now
213
+ JavaUtil::Date.from_ruby_date(DateTime.now)
214
+ end
215
+
216
+ # Returns the Java package name for the full class_name, or nil if
217
+ # class_name is unqualified.
218
+ def self.java_package_name(class_name)
219
+ prefix = class_name[/(\w+\.)+/]
220
+ # remove the trailing period
221
+ prefix.chop! if prefix
222
+ prefix
223
+ end
224
+ end
225
+
226
+ class Class
227
+ # Returns whether this is a Java wrapper class.
228
+ def java_class?
229
+ method_defined?(:java_class)
230
+ end
231
+
232
+ # Returns a Ruby class for the given klass. If klass is already a Ruby Class, then returns klass.
233
+ # If klass is a String, then returns the Ruby wrapper class for the corresponding Java class name.
234
+ # Otherwise, this method returns the Ruby class for the name of the presumed Java klass.
235
+ def self.to_ruby(klass)
236
+ case klass
237
+ when Class then klass
238
+ when String then Java.module_eval(klass)
239
+ else to_ruby(klass.name)
240
+ end
241
+ end
242
+
243
+ # Returns whether this class is abstract.
244
+ def abstract?
245
+ java_class? and Java::JavaLangReflect::Modifier.isAbstract(java_class.modifiers)
246
+ end
247
+
248
+ # Returns whether the given PropertyDescriptor pd corresponds to a transient field in this class, or nil if there is no such field.
249
+ def transient?(pd)
250
+ begin
251
+ field = java_class.declared_field(pd.name)
252
+ rescue Exception
253
+ # should occur only if a property is not a field; not an error
254
+ return
255
+ end
256
+ Java::JavaLangReflect::Modifier.isTransient(field.modifiers) if field
257
+ end
258
+
259
+ # Returns this class's readable and writable Java PropertyDescriptors, or an empty Array if none.
260
+ # If the hierarchy flag is set to +false+, then only this class's properties
261
+ # will be introspected.
262
+ def java_properties(hierarchy=true)
263
+ info = hierarchy ? Java::JavaBeans::Introspector.getBeanInfo(java_class) : Java::JavaBeans::Introspector.getBeanInfo(java_class, java_class.superclass)
264
+ info.propertyDescriptors.select { |pd| pd.write_method and property_read_method(pd) }
265
+ end
266
+
267
+ # Redefines the reserved method corresponeding to the given Java property descriptor pd
268
+ # back to the Object implementation, if necessary.
269
+ # If both this class and Object define a method with the property name,
270
+ # then a new method is defined with the same body as the previous method.
271
+ # Returns the new method symbol, or nil if name_or_symbol is not an occluded
272
+ # Object instance method.
273
+ #
274
+ # This method undoes the jRuby clobbering of Object methods by Java property method
275
+ # wrappers. The method is renamed as follows:
276
+ # * +id+ is changed to :identifier
277
+ # * +type+ is prefixed by the underscore subject class name, e.g. +Specimen.type => :specimen_type+,
278
+ # If the property name is +type+ and the subject class name ends in 'Type', then the attribute
279
+ # symbol is the underscore subject class name, e.g. +HistologicType.type => :histologic_type+.
280
+ #
281
+ # Raises ArgumentError if symbol is not an Object method.
282
+ def unocclude_reserved_method(pd)
283
+ oldname = pd.name.underscore
284
+ return unless OBJ_INST_MTHDS.include?(oldname)
285
+ oldsym = oldname.to_sym
286
+ undeprecated = case oldsym
287
+ when :id then :object_id
288
+ when :type then :class
289
+ else oldsym
290
+ end
291
+ rsvd_mth = Object.instance_method(undeprecated)
292
+ base = self.qp.underscore
293
+ newname = if oldname == 'id' then
294
+ 'identifier'
295
+ elsif base[-oldname.length..-1] == oldname then
296
+ base
297
+ else
298
+ "#{base}_#{oldname}"
299
+ end
300
+ newsym = newname.to_sym
301
+ rdr = property_read_method(pd).name.to_sym
302
+ alias_method(newsym, rdr)
303
+ # alias the writers
304
+ wtr = pd.write_method.name.to_sym
305
+ alias_method("#{newsym}=".to_sym, wtr)
306
+ # alias a camel-case Java-style method if necessary
307
+ altname = newname.camelize
308
+ unless altname == newname then
309
+ alias_method(altname.to_sym, oldsym)
310
+ alias_method("#{altname}=".to_sym, wtr)
311
+ end
312
+ # restore the old method to Object
313
+ define_method(oldsym) { |*args| rsvd_mth.bind(self).call(*args) }
314
+ newsym
315
+ end
316
+
317
+ # Returns the property descriptor pd introspected or discovered Java read Method.
318
+ def property_read_method(pd)
319
+ return pd.read_method if pd.read_method
320
+ # caCORE alert - java.lang.Boolean is<name> is not introspected as a read method, since type must be primitive boolean is<name>
321
+ return unless pd.get_property_type == Java::JavaLang::Boolean.java_class
322
+ rdr = java_class.java_method("is#{pd.name.capitalize_first}") rescue nil
323
+ logger.debug { "Discovered #{qp} #{pd.name} property non-introspected reader method #{rdr.name}." } if rdr
324
+ rdr
325
+ end
326
+
327
+ private
328
+
329
+ OBJ_INST_MTHDS = Object.instance_methods
330
+ end
331
+
332
+ class Array
333
+ alias :equal__base :==
334
+ # Overrides the standard == to compare a Java List with a Ruby Array.
335
+ def ==(other)
336
+ Java::JavaUtil::List === other ? other == self : equal__base(other)
337
+ end
338
+ end
@@ -0,0 +1,167 @@
1
+ require 'caruby/migration/resource_module'
2
+
3
+ module CaRuby
4
+ # A Migratable mix-in adds migration support for Resource domain objects.
5
+ # For each migration Resource created by a CaRuby::Migrator, the migration process
6
+ # is as follows:
7
+ #
8
+ # 1. The migrator creates the Resource using the empty constructor.
9
+ #
10
+ # 2. Each input field value which maps to a Resource attribute is obtained from the
11
+ # migration source.
12
+ #
13
+ # 3. If the Resource class implements a method +migrate_+_attribute_ for the
14
+ # migration _attribute_, then that migrate method is called with the input value
15
+ # argument. If there is a migrate method, then the attribute is set to the
16
+ # result of calling that method, otherwise the attribute is set to the original
17
+ # input value.
18
+ #
19
+ # For example, if the +Name+ input field maps to +Participant.name+, then a
20
+ # custom +Participant+ +migrate_name+ shim method can be defined to reformat
21
+ # the input name.
22
+ #
23
+ # 4. The Resource attribute is set to the (possibly modified) value.
24
+ #
25
+ # 5. After all input fields are processed, then {#migration_valid?} is called to
26
+ # determine whether the migrated object can be used. {#migration_valid?} is true
27
+ # by default, but a migration shim can add a validation check,
28
+ # migrated Resource class to return false for special cases.
29
+ #
30
+ # For example, a custom +Participant+ +migration_valid?+ shim method can be
31
+ # defined to return whether there is a non-empty input field value.
32
+ #
33
+ # 6. After the migrated objects are validated, then the Migrator fills in
34
+ # dependency hierarchy gaps. For example, if the Resource class +Participant+
35
+ # owns the +enrollments+ dependent which in turn owns the +encounters+ dependent
36
+ # and the migration has created a +Participant+ and an +Encounter+ but no +Enrollment+,
37
+ # then an empty +Enrollment+ is created which is owned by the migrated +Participant+
38
+ # and owns the migrated +Encounter+.
39
+ #
40
+ # 7. After all dependencies are filled in, then the independent references are set
41
+ # for each created Resource (including the new dependents). If a created
42
+ # Resource has an independent non-collection Resource reference attribute
43
+ # and there is a migrated instance of that attribute type, then the attribute
44
+ # is set to that migrated instance.
45
+ #
46
+ # For example, if +Enrollment+ has a +study+ attribute and there is a
47
+ # single migrated +Study+ instance, then the +study+ attribute is set
48
+ # to that migrated +Study+ instance.
49
+ #
50
+ # If the referencing class implements a method +migrate_+_attribute_ for the
51
+ # migration _attribute_, then that migrate method is called with the referenced
52
+ # instance argument. The result is used to set the attribute. Otherwise, the
53
+ # attribute is set to the original referenced instance.
54
+ #
55
+ # There must be a single unambiguous candidate independent instance, e.g. in the
56
+ # unlikely but conceivable case that two +Study+ instances are migrated, then the
57
+ # +study+ attribute is not set. Similarly, collection attributes are not set,
58
+ # e.g. a +Study+ +protocols+ attribute is not set to a migrated +Protocol+
59
+ # instance.
60
+ #
61
+ # 8. The {#migrate} method is called to complete the migration. As described in the
62
+ # method documentation, a migration shim Resource subclass can override the
63
+ # method for custom migration processing, e.g. to migrate the ambiguous or
64
+ # collection attributes mentioned above, or to fill in missing values.
65
+ #
66
+ # Note that there is an extensive set of attribute defaults defined in
67
+ # the CaRuby::ResourceMetadata application domain classes. These defaults
68
+ # are applied in a migration database save action and need not be set in
69
+ # a migration shim. For example, if an acceptable default for a +Study+
70
+ # +active?+ flag is defined in the +Study+ meta-data, then the flag does not
71
+ # need to be set in a migration shim.
72
+ module Migratable
73
+ # Completes setting this Migratable domain object's attributes from the given input row.
74
+ # This method is responsible for migrating attributes which are not mapped
75
+ # in the configuration. It is called after the configuration attributes for
76
+ # the given row are migrated and before {#migrate_references}.
77
+ #
78
+ # This base implementation is a no-op.
79
+ # Subclasses can modify this method to complete the migration. The overridden
80
+ # methods should call +super+ to pick up the superclass migration.
81
+ #
82
+ # @param [Hash] row the input row
83
+ # @param [Enumerable] migrated the migrated instances, including this Resource
84
+ def migrate(row, migrated)
85
+ end
86
+
87
+ # Returns whether this migration target domain object is valid. The default is true
88
+ # if this domain object either has no owner or its owner is valid.
89
+ # A migration shim should override this method on the target if there are conditions
90
+ # which determine whether the migration should be skipped for this target object.
91
+ #
92
+ # @return [Boolean] whether this migration target domain object is valid
93
+ def migration_valid?
94
+ # check that the owner is be valid
95
+ ownr = owner
96
+ ownr.nil? or ownr.migration_valid?
97
+ end
98
+
99
+ # Migrates this domain object's migratable references. This method is called by the
100
+ # CaRuby::Migrator and should not be overridden by subclasses. Subclasses tailor
101
+ # individual reference attribute migration by defining a +migrate_+_attribute_ method
102
+ # for the _attribute_ to modify.
103
+ #
104
+ # The migratable reference attributes consist of the non-collection
105
+ # {ResourceAttributes#saved_independent_attributes} which don't already have a value.
106
+ # For each such migratable attribute, if there is a single instance of the attribute
107
+ # type in the given migrated domain objects, then the attribute is set to that
108
+ # migrated instance.
109
+ #
110
+ # If the attribute is associated with a method in mth_hash, then that method is called
111
+ # on the migrated instance and input row. The attribute is set to the method return value.
112
+ # mth_hash includes an entry for each +migrate_+_attribute_ method defined by this
113
+ # Resource's class.
114
+ #
115
+ # @param [{Symbol => Object}] row the input row field => value hash
116
+ # @param [<Resource>] migrated the migrated instances, including this Resource
117
+ # @param [{Symbol => String}] mth_hash a hash that associates this domain object's
118
+ # attributes to migration method names
119
+ def migrate_references(row, migrated, mth_hash=nil)
120
+ self.class.saved_independent_attributes.each do |attr|
121
+ ref = migratable__reference_value(attr, migrated)
122
+ migratable__set_reference(attr, ref, row, mth_hash) if ref
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ # @param [Symbol] attribute the reference attribute to get
129
+ # @param migrated (see #migrate_references)
130
+ # @return [Resource, nil] the migrated value to which the attribute will be set
131
+ def migratable__reference_value(attribute, migrated)
132
+ # skip non-nil attributes
133
+ return if send(attribute)
134
+ # the attribute metadata, used for type information
135
+ attr_md = self.class.attribute_metadata(attribute)
136
+ # skip collection attributes
137
+ return if attr_md.collection?
138
+ # the migrated references which are instances of the attribute type
139
+ refs = migrated.select { |other| other != self and attr_md.type === other }
140
+ # skip ambiguous references
141
+ return unless refs.size == 1
142
+ # the single reference
143
+ ref = refs.first
144
+ end
145
+
146
+ # Sets the given migrated domain object attribute to the given reference.
147
+ #
148
+ # If the attribute is associated to a method in mth_hash, then that method is called on
149
+ # the migrated instance and input row. The attribute is set to the method return value.
150
+ # mth_hash includes an entry for each +migrate_+_attribute_ method defined by this
151
+ # Resource's class.
152
+ #
153
+ # @param [Symbol] (see #migratable__reference_value)
154
+ # @param [Resource] ref the migrated reference
155
+ # @param row (see #migrate_references)
156
+ # @param mth_hash (see #migrate_references)
157
+ def migratable__set_reference(attribute, ref, row, mth_hash=nil)
158
+ # the shim method, if any
159
+ mth = mth_hash[attribute] if mth_hash
160
+ # if there is a shim method, then call it
161
+ ref = send(mth, ref, row) if mth and respond_to?(mth)
162
+ return if ref.nil?
163
+ logger.debug { "Setting #{qp} #{attribute} to migrated #{ref.qp}..." }
164
+ set_attribute(attribute, ref)
165
+ end
166
+ end
167
+ end