caruby-core 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +51 -0
  5. data/doc/website/css/site.css +1 -5
  6. data/doc/website/images/avatar.png +0 -0
  7. data/doc/website/images/favicon.ico +0 -0
  8. data/doc/website/images/logo.png +0 -0
  9. data/doc/website/index.html +82 -0
  10. data/doc/website/install.html +87 -0
  11. data/doc/website/quick_start.html +87 -0
  12. data/doc/website/tissue.html +85 -0
  13. data/doc/website/uom.html +10 -0
  14. data/lib/caruby.rb +3 -0
  15. data/lib/caruby/active_support/README.txt +2 -0
  16. data/lib/caruby/active_support/core_ext/string.rb +7 -0
  17. data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
  18. data/lib/caruby/active_support/inflections.rb +55 -0
  19. data/lib/caruby/active_support/inflector.rb +398 -0
  20. data/lib/caruby/cli/application.rb +36 -0
  21. data/lib/caruby/cli/command.rb +169 -0
  22. data/lib/caruby/csv/csv_mapper.rb +157 -0
  23. data/lib/caruby/csv/csvio.rb +185 -0
  24. data/lib/caruby/database.rb +252 -0
  25. data/lib/caruby/database/fetched_matcher.rb +66 -0
  26. data/lib/caruby/database/persistable.rb +432 -0
  27. data/lib/caruby/database/persistence_service.rb +162 -0
  28. data/lib/caruby/database/reader.rb +599 -0
  29. data/lib/caruby/database/saved_merger.rb +131 -0
  30. data/lib/caruby/database/search_template_builder.rb +59 -0
  31. data/lib/caruby/database/sql_executor.rb +75 -0
  32. data/lib/caruby/database/store_template_builder.rb +200 -0
  33. data/lib/caruby/database/writer.rb +469 -0
  34. data/lib/caruby/domain/annotatable.rb +25 -0
  35. data/lib/caruby/domain/annotation.rb +23 -0
  36. data/lib/caruby/domain/attribute_metadata.rb +447 -0
  37. data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
  38. data/lib/caruby/domain/merge.rb +91 -0
  39. data/lib/caruby/domain/properties.rb +95 -0
  40. data/lib/caruby/domain/reference_visitor.rb +289 -0
  41. data/lib/caruby/domain/resource_attributes.rb +528 -0
  42. data/lib/caruby/domain/resource_dependency.rb +205 -0
  43. data/lib/caruby/domain/resource_introspection.rb +159 -0
  44. data/lib/caruby/domain/resource_metadata.rb +117 -0
  45. data/lib/caruby/domain/resource_module.rb +285 -0
  46. data/lib/caruby/domain/uniquify.rb +38 -0
  47. data/lib/caruby/import/annotatable_class.rb +28 -0
  48. data/lib/caruby/import/annotation_class.rb +27 -0
  49. data/lib/caruby/import/annotation_module.rb +67 -0
  50. data/lib/caruby/import/java.rb +338 -0
  51. data/lib/caruby/migration/migratable.rb +167 -0
  52. data/lib/caruby/migration/migrator.rb +533 -0
  53. data/lib/caruby/migration/resource.rb +8 -0
  54. data/lib/caruby/migration/resource_module.rb +11 -0
  55. data/lib/caruby/migration/uniquify.rb +20 -0
  56. data/lib/caruby/resource.rb +969 -0
  57. data/lib/caruby/util/attribute_path.rb +46 -0
  58. data/lib/caruby/util/cache.rb +53 -0
  59. data/lib/caruby/util/class.rb +99 -0
  60. data/lib/caruby/util/collection.rb +1053 -0
  61. data/lib/caruby/util/controlled_value.rb +35 -0
  62. data/lib/caruby/util/coordinate.rb +75 -0
  63. data/lib/caruby/util/domain_extent.rb +49 -0
  64. data/lib/caruby/util/file_separator.rb +65 -0
  65. data/lib/caruby/util/inflector.rb +20 -0
  66. data/lib/caruby/util/log.rb +95 -0
  67. data/lib/caruby/util/math.rb +12 -0
  68. data/lib/caruby/util/merge.rb +59 -0
  69. data/lib/caruby/util/module.rb +34 -0
  70. data/lib/caruby/util/options.rb +92 -0
  71. data/lib/caruby/util/partial_order.rb +36 -0
  72. data/lib/caruby/util/person.rb +119 -0
  73. data/lib/caruby/util/pretty_print.rb +184 -0
  74. data/lib/caruby/util/properties.rb +112 -0
  75. data/lib/caruby/util/stopwatch.rb +66 -0
  76. data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
  77. data/lib/caruby/util/transitive_closure.rb +45 -0
  78. data/lib/caruby/util/tree.rb +48 -0
  79. data/lib/caruby/util/trie.rb +37 -0
  80. data/lib/caruby/util/uniquifier.rb +30 -0
  81. data/lib/caruby/util/validation.rb +48 -0
  82. data/lib/caruby/util/version.rb +56 -0
  83. data/lib/caruby/util/visitor.rb +351 -0
  84. data/lib/caruby/util/weak_hash.rb +36 -0
  85. data/lib/caruby/version.rb +3 -0
  86. metadata +186 -0
@@ -0,0 +1,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