caruby-core 1.5.5 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. data/Gemfile +9 -0
  2. data/History.md +5 -1
  3. data/lib/caruby.rb +3 -5
  4. data/lib/caruby/caruby-src.tar.gz +0 -0
  5. data/lib/caruby/database.rb +53 -69
  6. data/lib/caruby/database/application_service.rb +25 -0
  7. data/lib/caruby/database/cache.rb +60 -0
  8. data/lib/caruby/database/fetched_matcher.rb +52 -38
  9. data/lib/caruby/database/lazy_loader.rb +4 -4
  10. data/lib/caruby/database/operation.rb +34 -0
  11. data/lib/caruby/database/persistable.rb +171 -86
  12. data/lib/caruby/database/persistence_service.rb +32 -34
  13. data/lib/caruby/database/persistifier.rb +100 -43
  14. data/lib/caruby/database/reader.rb +107 -85
  15. data/lib/caruby/database/reader_template_builder.rb +60 -0
  16. data/lib/caruby/database/saved_matcher.rb +3 -3
  17. data/lib/caruby/database/sql_executor.rb +88 -17
  18. data/lib/caruby/database/writer.rb +213 -177
  19. data/lib/caruby/database/writer_template_builder.rb +334 -0
  20. data/lib/caruby/{util → helpers}/controlled_value.rb +0 -0
  21. data/lib/caruby/{util → helpers}/coordinate.rb +4 -4
  22. data/lib/caruby/{util → helpers}/person.rb +3 -3
  23. data/lib/caruby/{util → helpers}/properties.rb +7 -9
  24. data/lib/caruby/{util → helpers}/roman.rb +2 -2
  25. data/lib/caruby/{util → helpers}/version.rb +1 -1
  26. data/lib/caruby/json/deserializer.rb +2 -2
  27. data/lib/caruby/json/serializer.rb +49 -7
  28. data/lib/caruby/metadata.rb +30 -0
  29. data/lib/caruby/metadata/java_property.rb +21 -0
  30. data/lib/caruby/metadata/propertied.rb +191 -0
  31. data/lib/caruby/metadata/property.rb +22 -0
  32. data/lib/caruby/metadata/property_characteristics.rb +201 -0
  33. data/lib/caruby/migration/migratable.rb +11 -182
  34. data/lib/caruby/rdbi/driver/jdbc.rb +446 -0
  35. data/lib/caruby/resource.rb +20 -823
  36. data/lib/caruby/version.rb +1 -1
  37. data/test/lib/caruby/database/cache_test.rb +54 -0
  38. data/test/lib/caruby/{util → helpers}/controlled_value_test.rb +3 -5
  39. data/test/lib/caruby/{util → helpers}/person_test.rb +4 -6
  40. data/test/lib/caruby/helpers/properties_test.rb +34 -0
  41. data/test/lib/caruby/{util → helpers}/roman_test.rb +2 -3
  42. data/test/lib/caruby/{util → helpers}/version_test.rb +2 -3
  43. data/test/lib/helper.rb +7 -0
  44. metadata +161 -214
  45. data/lib/caruby/cli/application.rb +0 -36
  46. data/lib/caruby/cli/command.rb +0 -202
  47. data/lib/caruby/csv/csv_mapper.rb +0 -159
  48. data/lib/caruby/csv/csvio.rb +0 -203
  49. data/lib/caruby/database/search_template_builder.rb +0 -56
  50. data/lib/caruby/database/store_template_builder.rb +0 -278
  51. data/lib/caruby/domain.rb +0 -193
  52. data/lib/caruby/domain/attribute.rb +0 -584
  53. data/lib/caruby/domain/attributes.rb +0 -628
  54. data/lib/caruby/domain/dependency.rb +0 -225
  55. data/lib/caruby/domain/id_alias.rb +0 -22
  56. data/lib/caruby/domain/importer.rb +0 -183
  57. data/lib/caruby/domain/introspection.rb +0 -176
  58. data/lib/caruby/domain/inverse.rb +0 -172
  59. data/lib/caruby/domain/inversible.rb +0 -90
  60. data/lib/caruby/domain/java_attribute.rb +0 -173
  61. data/lib/caruby/domain/merge.rb +0 -185
  62. data/lib/caruby/domain/metadata.rb +0 -142
  63. data/lib/caruby/domain/mixin.rb +0 -35
  64. data/lib/caruby/domain/properties.rb +0 -95
  65. data/lib/caruby/domain/reference_visitor.rb +0 -428
  66. data/lib/caruby/domain/uniquify.rb +0 -50
  67. data/lib/caruby/import/java.rb +0 -387
  68. data/lib/caruby/migration/migrator.rb +0 -918
  69. data/lib/caruby/migration/resource_module.rb +0 -9
  70. data/lib/caruby/migration/uniquify.rb +0 -17
  71. data/lib/caruby/util/attribute_path.rb +0 -44
  72. data/lib/caruby/util/cache.rb +0 -56
  73. data/lib/caruby/util/class.rb +0 -149
  74. data/lib/caruby/util/collection.rb +0 -1152
  75. data/lib/caruby/util/domain_extent.rb +0 -46
  76. data/lib/caruby/util/file_separator.rb +0 -65
  77. data/lib/caruby/util/inflector.rb +0 -27
  78. data/lib/caruby/util/log.rb +0 -95
  79. data/lib/caruby/util/math.rb +0 -12
  80. data/lib/caruby/util/merge.rb +0 -59
  81. data/lib/caruby/util/module.rb +0 -18
  82. data/lib/caruby/util/options.rb +0 -97
  83. data/lib/caruby/util/partial_order.rb +0 -35
  84. data/lib/caruby/util/pretty_print.rb +0 -204
  85. data/lib/caruby/util/stopwatch.rb +0 -74
  86. data/lib/caruby/util/topological_sync_enumerator.rb +0 -62
  87. data/lib/caruby/util/transitive_closure.rb +0 -55
  88. data/lib/caruby/util/tree.rb +0 -48
  89. data/lib/caruby/util/trie.rb +0 -37
  90. data/lib/caruby/util/uniquifier.rb +0 -30
  91. data/lib/caruby/util/validation.rb +0 -20
  92. data/lib/caruby/util/visitor.rb +0 -365
  93. data/lib/caruby/util/weak_hash.rb +0 -36
  94. data/test/lib/caruby/csv/csv_mapper_test.rb +0 -40
  95. data/test/lib/caruby/csv/csvio_test.rb +0 -69
  96. data/test/lib/caruby/database/persistable_test.rb +0 -92
  97. data/test/lib/caruby/domain/domain_test.rb +0 -112
  98. data/test/lib/caruby/domain/inversible_test.rb +0 -99
  99. data/test/lib/caruby/domain/reference_visitor_test.rb +0 -130
  100. data/test/lib/caruby/import/java_test.rb +0 -80
  101. data/test/lib/caruby/import/mixed_case_test.rb +0 -14
  102. data/test/lib/caruby/migration/test_case.rb +0 -102
  103. data/test/lib/caruby/test_case.rb +0 -230
  104. data/test/lib/caruby/util/cache_test.rb +0 -23
  105. data/test/lib/caruby/util/class_test.rb +0 -61
  106. data/test/lib/caruby/util/collection_test.rb +0 -398
  107. data/test/lib/caruby/util/command_test.rb +0 -55
  108. data/test/lib/caruby/util/domain_extent_test.rb +0 -60
  109. data/test/lib/caruby/util/file_separator_test.rb +0 -30
  110. data/test/lib/caruby/util/inflector_test.rb +0 -12
  111. data/test/lib/caruby/util/lazy_hash_test.rb +0 -34
  112. data/test/lib/caruby/util/merge_test.rb +0 -83
  113. data/test/lib/caruby/util/module_test.rb +0 -25
  114. data/test/lib/caruby/util/options_test.rb +0 -59
  115. data/test/lib/caruby/util/partial_order_test.rb +0 -42
  116. data/test/lib/caruby/util/pretty_print_test.rb +0 -85
  117. data/test/lib/caruby/util/properties_test.rb +0 -50
  118. data/test/lib/caruby/util/stopwatch_test.rb +0 -18
  119. data/test/lib/caruby/util/topological_sync_enumerator_test.rb +0 -69
  120. data/test/lib/caruby/util/transitive_closure_test.rb +0 -67
  121. data/test/lib/caruby/util/tree_test.rb +0 -23
  122. data/test/lib/caruby/util/trie_test.rb +0 -14
  123. data/test/lib/caruby/util/visitor_test.rb +0 -278
  124. data/test/lib/caruby/util/weak_hash_test.rb +0 -45
  125. data/test/lib/examples/clinical_trials/migration/migration_test.rb +0 -58
  126. data/test/lib/examples/clinical_trials/migration/test_case.rb +0 -38
@@ -1,50 +0,0 @@
1
- require 'singleton'
2
- require 'caruby/util/uniquifier'
3
- require 'caruby/util/collection'
4
-
5
- module CaRuby
6
- module Resource
7
- # The Unique mix-in makes values unique within the scope of a Resource class.
8
- module Unique
9
- # Makes the given String value unique in the context of this object's class.
10
- # @return nil if value is nil
11
- # Raises TypeError if value is neither nil nor a String.
12
- def uniquify_value(value)
13
- unless String === value or value.nil? then
14
- raise TypeError.new("Cannot uniquify #{qp} non-String value #{value}")
15
- end
16
- ResourceUniquifier.instance.uniquify(self, value)
17
- end
18
-
19
- # Makes the secondary key unique by replacing each String key attribute value
20
- # with a unique value.
21
- def uniquify
22
- self.class.secondary_key_attributes.each do |attr|
23
- oldval = send(attr)
24
- next unless String === oldval
25
- newval = uniquify_value(oldval)
26
- set_attribute(attr, newval)
27
- logger.debug { "Reset #{qp} #{attr} from #{oldval} to unique value #{newval}." }
28
- end
29
- end
30
- end
31
- end
32
-
33
- # The ResourceUniquifier singleton makes Resource instance attribute values unique.
34
- class ResourceUniquifier
35
- include Singleton
36
-
37
- def initialize
38
- @cache = LazyHash.new { Hash.new }
39
- end
40
-
41
- # Makes the obj attribute value unique, or returns nil if value is nil.
42
- def uniquify(obj, value)
43
- @cache[obj.class][value] ||= value.uniquify if value
44
- end
45
-
46
- def clear
47
- @cache.clear
48
- end
49
- end
50
- end
@@ -1,387 +0,0 @@
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/log'
12
- require 'caruby/util/inflector'
13
- require 'caruby/util/collection'
14
-
15
- module Java
16
- private
17
-
18
- # The Windows semi-colon path separator.
19
- WINDOWS_PATH_SEP = ';'
20
-
21
- # The Unix colon path separator.
22
- UNIX_PATH_SEP = ':'
23
-
24
- public
25
-
26
- # Adds the directories in the given path and all Java jar files contained in the directories
27
- # to the execution classpath.
28
- #
29
- # @param [String] path the colon or semi-colon separated directories
30
- def self.add_path(path)
31
- # the path separator
32
- sep = path[WINDOWS_PATH_SEP] ? WINDOWS_PATH_SEP : UNIX_PATH_SEP
33
- # the path directories
34
- dirs = path.split(sep).map { |dir| File.expand_path(dir) }
35
- # Add all jars found anywhere within the directories to the the classpath.
36
- add_jars(*dirs)
37
- # Add the directories to the the classpath.
38
- dirs.each { |dir| add_to_classpath(dir) }
39
- end
40
-
41
- # Adds the jars in the directories to the execution class path.
42
- #
43
- # @param [<String>] directories the directories containing jars to add
44
- def self.add_jars(*directories)
45
- directories.each do |dir|
46
- Dir[File.join(dir , "**", "*.jar")].each { |jar| add_to_classpath(jar) }
47
- end
48
- end
49
-
50
- # Adds the given jar file or directory to the classpath.
51
- #
52
- # @param [String] file the jar file or directory to add
53
- def self.add_to_classpath(file)
54
- unless File.exist?(file) then
55
- logger.warn("File to place on Java classpath does not exist: #{file}")
56
- return
57
- end
58
- if file =~ /.jar$/ then
59
- # require is preferred to classpath append for a jar file
60
- require file
61
- else
62
- # A directory must end in a slash since JRuby uses an URLClassLoader.
63
- if File.directory?(file) and not file =~ /\/$/ then file = file + '/' end
64
- $CLASSPATH << file
65
- end
66
- end
67
-
68
- module JavaUtil
69
- # Aliases Java Collection methods with the standard Ruby Set counterpart, e.g. +delete+ for +remove+.
70
- module Collection
71
- def to_a
72
- inject(Array.new) { |array, item| array << item }
73
- end
74
-
75
- # Removes the given item from this collection.
76
- def delete(item)
77
- # can't alias delete to remove, since a Java interface doesn't implement any methods
78
- remove(item)
79
- end
80
-
81
- # Removes the items from this collection for which the block given to this method returns a non-nil, non-false value.
82
- def delete_if
83
- removeAll(select { |item| yield item })
84
- self
85
- end
86
- end
87
-
88
- # Aliases Java List methods with the standard Ruby Array counterpart, e.g. +merge+ for +addAll+.
89
- module List
90
- # Returns whether this List has the same content as the other Java List or Ruby Array.
91
- def ==(other)
92
- Array === other ? to_a == other : equals(other)
93
- end
94
-
95
- # Removes the given item from this collection.
96
- def delete(item)
97
- remove(item)
98
- end
99
- end
100
-
101
- module Map
102
- # Returns whether this Set has the same content as the other Java Map or Ruby Hash.
103
- def ==(other)
104
- ::Hash === other ? (size == other.size and other.all? { |key, value| get(key) == value }) : equals(other)
105
- end
106
-
107
- # Merges the other Java Map or Ruby Hash into this Map. Returns this modified Map.
108
- #
109
- # If a block is given to this method, then the block determines the mapped value
110
- # as specified in the Ruby Hash merge method documentation.
111
- def merge(other)
112
- other.each do |key, value|
113
- value = yield(key, get(key), value) if block_given? and containsKey(key)
114
- put(key, value)
115
- end
116
- self
117
- end
118
-
119
- alias :merge! :merge
120
- end
121
-
122
- module Set
123
- # Returns whether this Set has the same content as the other Java Set or Ruby Set.
124
- def ==(other)
125
- ::Set === other ? (size == other.size and all? { |item| other.include?(item) }) : equals(other)
126
- end
127
-
128
- # Merges the other Enumerable into this Set. Returns this modified Set.
129
- #
130
- # This method conforms to the Ruby Set merge contract rather than the Ruby List and Hash
131
- # merge contract. Ruby Set merge modifies the Set in-place, whereas Ruby List and Hash
132
- # merge return a new collection.
133
- def merge(other)
134
- return self if other.nil?
135
- raise ArgumentError.new("Merge argument must be enumerable: #{other}") unless Enumerable === other
136
- other.each { |item| self << item }
137
- self
138
- end
139
-
140
- alias :merge! :merge
141
- end
142
-
143
- class HashSet
144
- alias :base__clear :clear
145
- private :base__clear
146
- def clear
147
- base__clear
148
- self
149
- end
150
- end
151
-
152
- class TreeSet
153
- alias :base__first :first
154
- private :base__first
155
- # Fixes the jRuby {TreeSet#first} to return nil on an empty set rather than raise a Java exception.
156
- def first
157
- empty? ? nil : base__first
158
- end
159
- end
160
-
161
- class ArrayList
162
- alias :base__clear :clear
163
- private :base__clear
164
- def clear
165
- base__clear
166
- self
167
- end
168
- end
169
-
170
- class Date
171
- # The millisecond-to-day conversion factor.
172
- MILLIS_PER_DAY = (60 * 60 * 1000) * 24
173
-
174
- # Converts this Java Date to a Ruby DateTime.
175
- #
176
- # @quirk caTissue Bug #165: API CPR create date validation is time zone dependent.
177
- # Since Java Date accounts for DST and Ruby DateTime doesn't, this method makes the
178
- # DST adjustment by subtracting a compensatory one-hour DST offset from the
179
- # Java Date time zone offset and using that to set the DateTime offset.
180
- # This ensures that Date conversion is idempotent, i.e.
181
- # date.to_ruby_date().to_java_date == date
182
- #
183
- # However, there can be adverse consequences for an application that assumes that the
184
- # client time zone is the same as the server time zone, as described in caTissue
185
- # Bug #165.
186
- #
187
- # @return [DateTime] the Ruby date
188
- def to_ruby_date
189
- calendar = java.util.Calendar.instance
190
- calendar.setTime(self)
191
- secs = calendar.timeInMillis.to_f / 1000
192
- # millis since epoch
193
- time = Time.at(secs)
194
- # convert UTC timezone millisecond offset to Rational fraction of a day
195
- offset_millis = calendar.timeZone.getOffset(calendar.timeInMillis).to_f
196
- if offset_millis.zero? then
197
- offset = 0
198
- else
199
- offset_days = offset_millis / MILLIS_PER_DAY
200
- offset_fraction = 1 / offset_days
201
- offset = Rational(1, offset_fraction)
202
- end
203
- # convert to DateTime
204
- DateTime.civil(time.year, time.mon, time.day, time.hour, time.min, time.sec, offset)
205
- end
206
-
207
- # Converts a Ruby Date or DateTime to a Java Date.
208
- #
209
- # @param [::Date, DateTime] date the Ruby date
210
- # @return [Date] the Java date
211
- def self.from_ruby_date(date)
212
- return if date.nil?
213
- # DateTime has time attributes, Date doesn't
214
- if DateTime === date then
215
- hour, min, sec = date.hour, date.min, date.sec
216
- else
217
- hour = min = sec = 0
218
- end
219
- # the Ruby time
220
- rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, nil, nil)
221
- # millis since epoch
222
- millis = (rtime.to_f * 1000).truncate
223
- # the Java date factory
224
- calendar = java.util.Calendar.instance
225
- calendar.setTimeInMillis(millis)
226
- jtime = calendar.getTime
227
- # the daylight time flag
228
- isdt = calendar.timeZone.inDaylightTime(jtime)
229
- return jtime unless isdt
230
- # adjust the Ruby time for DST
231
- rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, isdt, nil)
232
- millis = (rtime.to_f * 1000).truncate
233
- calendar.setTimeInMillis(millis)
234
- calendar.getTime
235
- end
236
- end
237
- end
238
-
239
- def self.now
240
- JavaUtil::Date.from_ruby_date(DateTime.now)
241
- end
242
-
243
- # @param [Class, String] the JRuby class or the full Java class name
244
- # @return (String, String] the package and base for the given name
245
- def self.split_class_name(name_or_class)
246
- name = Class === name_or_class ? name_or_class.java_class.name : name_or_class
247
- match = NAME_SPLITTER_REGEX.match(name)
248
- match ? match.captures : [nil, name]
249
- end
250
-
251
- private
252
-
253
- NAME_SPLITTER_REGEX = /^([\w.]+)\.(\w+)$/
254
- end
255
-
256
- class Class
257
- # Returns whether this is a Java wrapper class.
258
- def java_class?
259
- method_defined?(:java_class)
260
- end
261
-
262
- # Returns the Ruby class for the given class, as follows:
263
- # * If the given class is already a Ruby class, then return the class.
264
- # * If the class argument is a Java class or a Java class name, then
265
- # the Ruby class is the JRuby wrapper for the Java class.
266
- #
267
- # @param [Class, String] class_or_name the class or class name
268
- # @return [Class] the corresponding Ruby class
269
- def self.to_ruby(class_or_name)
270
- case class_or_name
271
- when Class then class_or_name
272
- when String then eval to_ruby_name(class_or_name)
273
- else to_ruby(class_or_name.name)
274
- end
275
- end
276
-
277
- # @return [Boolean] whether this is a wrapper for an abstract Java class
278
- def abstract?
279
- java_class? and Java::JavaLangReflect::Modifier.isAbstract(java_class.modifiers)
280
- end
281
-
282
- # Returns whether the given PropertyDescriptor pd corresponds to a transient field in this class, or nil if there is no such field.
283
- def transient?(pd)
284
- begin
285
- field = java_class.declared_field(pd.name)
286
- rescue Exception
287
- # should occur only if a property is not a field; not an error
288
- return
289
- end
290
- Java::JavaLangReflect::Modifier.isTransient(field.modifiers) if field
291
- end
292
-
293
- # Returns this class's readable and writable Java PropertyDescriptors, or an empty Array if none.
294
- # If the hierarchy flag is set to +false+, then only this class's properties
295
- # will be introspected.
296
- def java_properties(hierarchy=true)
297
- return Array::EMPTY_ARRAY unless java_class?
298
- info = hierarchy ? Java::JavaBeans::Introspector.getBeanInfo(java_class) : Java::JavaBeans::Introspector.getBeanInfo(java_class, java_class.superclass)
299
- info.propertyDescriptors.select { |pd| pd.write_method and property_read_method(pd) }
300
- end
301
-
302
- # Redefines the reserved method corresponeding to the given Java property descriptor pd
303
- # back to the Object implementation, if necessary.
304
- # If both this class and Object define a method with the property name,
305
- # then a new method is defined with the same body as the previous method.
306
- # Returns the new method symbol, or nil if name_or_symbol is not an occluded
307
- # Object instance method.
308
- #
309
- # This method undoes the jRuby clobbering of Object methods by Java property method
310
- # wrappers. The method is renamed as follows:
311
- # * +id+ is changed to :identifier
312
- # * +type+ is prefixed by the underscore subject class name, e.g. +Specimen.type => :specimen_type+,
313
- # If the property name is +type+ and the subject class name ends in 'Type', then the attribute
314
- # symbol is the underscore subject class name, e.g. +HistologicType.type => :histologic_type+.
315
- #
316
- # Raises ArgumentError if symbol is not an Object method.
317
- def unocclude_reserved_method(pd)
318
- oldname = pd.name.underscore
319
- return unless OBJ_INST_MTHDS.include?(oldname)
320
- oldsym = oldname.to_sym
321
- undeprecated = case oldsym
322
- when :id then :object_id
323
- when :type then :class
324
- else oldsym
325
- end
326
- rsvd_mth = Object.instance_method(undeprecated)
327
- base = self.qp.underscore
328
- newname = if oldname == 'id' then
329
- 'identifier'
330
- elsif base[-oldname.length..-1] == oldname then
331
- base
332
- else
333
- "#{base}_#{oldname}"
334
- end
335
- newsym = newname.to_sym
336
- rdr = property_read_method(pd).name.to_sym
337
- alias_method(newsym, rdr)
338
- # alias the writers
339
- wtr = pd.write_method.name.to_sym
340
- alias_method("#{newsym}=".to_sym, wtr)
341
- # alias a camel-case Java-style method if necessary
342
- altname = newname.camelize
343
- unless altname == newname then
344
- alias_method(altname.to_sym, oldsym)
345
- alias_method("#{altname}=".to_sym, wtr)
346
- end
347
- # restore the old method to Object
348
- define_method(oldsym) { |*args| rsvd_mth.bind(self).call(*args) }
349
- newsym
350
- end
351
-
352
- # @quirk caCORE java.lang.Boolean is<name> is not introspected as a read method, since the type
353
- # must be primitive, i.e. `boolean is`<name>.
354
- #
355
- # @return [Symbol] the property descriptor pd introspected or discovered Java read Method
356
- def property_read_method(pd)
357
- return pd.read_method if pd.read_method
358
- return unless pd.get_property_type == Java::JavaLang::Boolean.java_class
359
- rdr = java_class.java_method("is#{pd.name.capitalize_first}") rescue nil
360
- logger.debug { "Discovered #{qp} #{pd.name} property non-introspected reader method #{rdr.name}." } if rdr
361
- rdr
362
- end
363
-
364
- private
365
-
366
- OBJ_INST_MTHDS = Object.instance_methods
367
-
368
- # @param [String] jname the fully-qualified Java class or interface name
369
- # @return [String] the JRuby class or module name
370
- # #example
371
- # Java.to_ruby_class_name('com.test.Sample') #=> Java::ComTest::Sample
372
- def self.to_ruby_name(jname)
373
- path = jname.split('.')
374
- return "Java::#{jname}" if path.size == 1
375
- cname = path[-1]
376
- pkg = path[0...-1].map { |s| s.capitalize_first }.join
377
- "Java::#{pkg}::#{cname}"
378
- end
379
- end
380
-
381
- class Array
382
- alias :equal__base :==
383
- # Overrides the standard == to compare a Java List with a Ruby Array.
384
- def ==(other)
385
- Java::JavaUtil::List === other ? other == self : equal__base(other)
386
- end
387
- end
@@ -1,918 +0,0 @@
1
- # load the required gems
2
- require 'rubygems'
3
-
4
- # the Units of Measurement gem
5
- gem 'uom'
6
-
7
- require 'enumerator'
8
- require 'date'
9
- require 'uom'
10
- require 'caruby/csv/csvio'
11
- require 'caruby/util/class'
12
- require 'caruby/util/log'
13
- require 'caruby/util/inflector'
14
- require 'caruby/util/options'
15
- require 'caruby/util/pretty_print'
16
- require 'caruby/util/properties'
17
- require 'caruby/util/collection'
18
- require 'caruby/migration/migratable'
19
-
20
- module CaRuby
21
- class MigrationError < RuntimeError; end
22
-
23
- # Migrates a CSV extract to a caBIG application.
24
- class Migrator
25
- include Enumerable
26
-
27
- # Creates a new Migrator.
28
- #
29
- # @param [{Symbol => Object}] opts the migration options
30
- # @option opts [String] :database required application {Database}
31
- # @option opts [String] :target required target domain class
32
- # @option opts [<String>, String] :mapping required input field => caTissue attribute mapping file(s)
33
- # @option opts [<String>, String] :defaults optional caTissue attribute => value default mapping file(s)
34
- # @option opts [<String>, String] :filters optional caTissue attribute input value => caTissue value filter file(s)
35
- # @option opts [String] :input required source file to migrate
36
- # @option opts [<String>, String] :shims optional shim file(s) to load
37
- # @option opts [String] :unique ensures that migrated objects which include the {Resource::Unique}
38
- # @option opts [String] :create optional flag indicating that existing target objects are ignored
39
- # @option opts [String] :bad optional invalid record file
40
- # @option opts [Integer] :offset zero-based starting source record number to process (default 0)
41
- # @option opts [Boolean] :quiet suppress output messages
42
- # @option opts [Boolean] :verbose print progress
43
- def initialize(opts)
44
- @rec_cnt = 0
45
- parse_options(opts)
46
- build
47
- end
48
-
49
- # Imports this migrator's file into the database with the given connect options.
50
- # This method creates or updates the domain objects mapped from the import source.
51
- # If a block is given to this method, then the block is called on each stored
52
- # migration target object.
53
- #
54
- # If the +:create+ option is set, then an input record for a target object which already
55
- # exists in the database is noted in a debug log message and ignored rather than updated.
56
- #
57
- # @yield [target] operation performed on the migration target
58
- # @yieldparam [Resource] target the migrated target domain object
59
- def migrate_to_database(&block)
60
- # migrate with save
61
- tm = Stopwatch.measure { execute_save(&block) }.elapsed
62
- logger.debug { format_migration_time_log_message(tm) }
63
- end
64
-
65
- # Imports this migrator's CSV file and calls the required block on each migrated target
66
- # domain object.
67
- #
68
- # @yield [target] operation performed on the migration target
69
- # @yieldparam [Resource] target the migrated target domain object
70
- def migrate(&block)
71
- raise MigrationError.new("The caRuby Migrator migrate block is missing") unless block_given?
72
- migrate_rows(&block)
73
- end
74
-
75
- alias :each :migrate
76
-
77
- private
78
-
79
- REGEXP_PAT = /^\/(.*[^\\])\/([inx]+)?$/
80
-
81
- # Class {#migrate} with a {#save} block.
82
- def execute_save
83
- if @database.nil? then
84
- raise MigrationError.new("Migrator cannot save records since the database option was not specified.")
85
- end
86
- @database.open do |db|
87
- migrate do |target|
88
- save(target, db)
89
- yield target if block_given?
90
- db.clear
91
- end
92
- end
93
- end
94
-
95
- # @return a log message String for the given migration time in seconds
96
- def format_migration_time_log_message(time)
97
- # the database execution time
98
- dt = @database.execution_time
99
- if time > 120 then
100
- time /= 60
101
- dt /= 60
102
- unit = "minutes"
103
- else
104
- unit = "seconds"
105
- end
106
- "Migration took #{'%.2f' % time} #{unit}, of which #{'%.2f' % dt} were database operations."
107
- end
108
-
109
- def parse_options(opts)
110
- @fld_map_files = opts[:mapping]
111
- raise MigrationError.new("Migrator missing required field mapping file parameter") if @fld_map_files.nil?
112
- @def_files = opts[:defaults]
113
- @filter_files = opts[:filters]
114
- @shims = opts[:shims] ||= []
115
- @offset = opts[:offset] ||= 0
116
- @input = Options.get(:input, opts)
117
- raise MigrationError.new("Migrator missing required source file parameter") if @input.nil?
118
- @database = opts[:database]
119
- @target_class = opts[:target]
120
- raise MigrationError.new("Migrator missing required target class parameter") if @target_class.nil?
121
- @bad_rec_file = opts[:bad]
122
- @create = opts[:create]
123
- logger.info("Migration options: #{printable_options(opts).pp_s}.")
124
- # flag indicating whether to print a progress monitor
125
- @print_progress = opts[:verbose]
126
- end
127
-
128
- def printable_options(opts)
129
- popts = opts.reject { |option, value| value.nil_or_empty? }
130
- # The target class should be a simple class name rather than the class metadata.
131
- popts[:target] = popts[:target].qp if popts.has_key?(:target)
132
- popts
133
- end
134
-
135
- def build
136
- # the current source class => instance map
137
- raise MigrationError.new("No file to migrate") if @input.nil?
138
-
139
- # make a CSV loader which only converts input fields corresponding to non-String attributes
140
- @loader = CsvIO.new(@input, &method(:convert))
141
- logger.debug { "Migration data input file #{@input} headers: #{@loader.headers.qp}" }
142
-
143
- # add shim modifiers
144
- load_shims(@shims)
145
-
146
- # create the class => path => default value hash
147
- @def_hash = @def_files ? load_defaults_files(@def_files) : {}
148
- # create the class => path => default value hash
149
- @filter_hash = @filter_files ? load_filter_files(@filter_files) : {}
150
- # create the class => path => header hash
151
- fld_map = load_field_map_files(@fld_map_files)
152
- # create the class => paths hash
153
- @cls_paths_hash = create_class_paths_hash(fld_map, @def_hash)
154
- # create the path => class => header hash
155
- @header_map = create_header_map(fld_map)
156
- # add missing owner classes (copy the keys rather than using each_key since the hash is updated)
157
- @owners = Set.new
158
- @cls_paths_hash.keys.each { |klass| add_owners(klass) }
159
- # order the creatable classes by dependency, owners first, to smooth the migration process
160
- @creatable_classes = @cls_paths_hash.keys.sort! { |klass, other| other.depends_on?(klass) ? -1 : (klass.depends_on?(other) ? 1 : 0) }
161
- @creatable_classes.each do |klass|
162
- if klass.abstract? then
163
- raise MigrationError.new("Migrator cannot create the abstract class #{klass}; specify a subclass instead in the mapping file.")
164
- end
165
- end
166
-
167
- # print the maps
168
- print_hash = LazyHash.new { Hash.new }
169
- @cls_paths_hash.each do |klass, paths|
170
- print_hash[klass.qp] = paths.map { |path| {path.map { |attr_md| attr_md.to_sym }.join('.') => @header_map[path][klass] } }
171
- end
172
- logger.info { "Migration paths:\n#{print_hash.pp_s}" }
173
- logger.info { "Migration creatable classes: #{@creatable_classes.qp}." }
174
- unless @def_hash.empty? then logger.info { "Migration defaults: #{@def_hash.qp}." } end
175
-
176
- # the class => attribute migration methods hash
177
- create_migration_method_hashes
178
-
179
- # Collect the String input fields for the custom CSVLoader converter.
180
- @nonstring_headers = Set.new
181
- logger.info("Migration attributes:")
182
- @header_map.each do |path, cls_hdr_hash|
183
- attr_md = path.last
184
- cls_hdr_hash.each do |klass, hdr|
185
- type_s = attr_md.type ? attr_md.type.qp : 'Object'
186
- logger.info(" #{hdr} => #{klass.qp}.#{path.join('.')} (#{type_s})")
187
- end
188
- @nonstring_headers.merge!(cls_hdr_hash.values) if attr_md.type != Java::JavaLang::String
189
- end
190
- end
191
-
192
- # Converts the given input field value as follows:
193
- # * if the info header is a String field, then return the value unchanged
194
- # * otherwise, if the value is a case-insensitive match for +true+ or +false+, then convert
195
- # the value to the respective Boolean
196
- # * otherwise, return nil which will delegate to the generic CsvIO converter
197
- # @param (see CsvIO#convert)
198
- # @yield (see CsvIO#convert)
199
- def convert(value, info)
200
- @nonstring_headers.include?(info.header) ? convert_boolean(value) : value
201
- end
202
-
203
- # @param [String] value the input value
204
- # @return [Boolean, nil] the corresponding boolean, or nil if none
205
- def convert_boolean(value)
206
- case value
207
- when /true/i then true
208
- when /false/i then false
209
- end
210
- end
211
-
212
- # Adds missing owner classes to the migration class path hash (with empty paths)
213
- # for the the given migration class.
214
- #
215
- # @param [Class] klass the migration class
216
- def add_owners(klass)
217
- owner = missing_owner_for(klass) || return
218
- logger.debug { "Migrator adding #{klass.qp} owner #{owner}" }
219
- @owners << owner
220
- @cls_paths_hash[owner] = Array::EMPTY_ARRAY
221
- add_owners(owner)
222
- end
223
-
224
- # @param [Class] klass the migration class
225
- # @return [Class, nil] the missing class owner, if any
226
- def missing_owner_for(klass)
227
- # check for an owner among the current migration classes
228
- return if klass.owners.any? do |owner|
229
- @cls_paths_hash.detect_key { |other| other <= owner }
230
- end
231
- # find the first non-abstract candidate owner
232
- klass.owners.detect { |owner| not owner.abstract? }
233
- end
234
-
235
- # Creates the class => +migrate_+_<attribute>_ hash for the given klasses.
236
- def create_migration_method_hashes
237
- # the class => attribute => migration filter hash
238
- @attr_flt_hash = {}
239
- customizable_class_attributes.each do |klass, attr_mds|
240
- flts = migration_filters(klass, attr_mds) || next
241
- @attr_flt_hash[klass] = flts
242
- end
243
-
244
- # print the migration shim methods
245
- unless @attr_flt_hash.empty? then
246
- printer_hash = LazyHash.new { Array.new }
247
- @attr_flt_hash.each do |klass, hash|
248
- mths = hash.values.select { |flt| Symbol === flt }
249
- printer_hash[klass.qp] = mths unless mths.empty?
250
- end
251
- logger.info("Migration shim methods: #{printer_hash.pp_s}.") unless printer_hash.empty?
252
- end
253
- end
254
-
255
- # @return the class => attributes hash for terminal path attributes which can be customized by +migrate_+ methods
256
- def customizable_class_attributes
257
- # The customizable classes set, starting with creatable classes and adding in
258
- # the migration path terminal attribute declarer classes below.
259
- klasses = @creatable_classes.to_set
260
- # the class => path terminal attributes hash
261
- cls_attrs_hash = LazyHash.new { Set.new }
262
- # add each path terminal attribute and its declarer class
263
- @cls_paths_hash.each_value do |paths|
264
- paths.each do |path|
265
- attr_md = path.last
266
- type = attr_md.declarer
267
- klasses << type
268
- cls_attrs_hash[type] << attr_md
269
- end
270
- end
271
-
272
- # Merge each redundant customizable superclass into its concrete customizable subclasses.
273
- klasses.dup.each do |cls|
274
- redundant = false
275
- klasses.each do |other|
276
- # cls is redundant if it is a superclass of other
277
- redundant = other < cls
278
- if redundant then
279
- cls_attrs_hash[other].merge!(cls_attrs_hash[cls])
280
- end
281
- end
282
- # remove the redundant class
283
- if redundant then
284
- cls_attrs_hash.delete(cls)
285
- klasses.delete(cls)
286
- end
287
- end
288
-
289
- cls_attrs_hash
290
- end
291
-
292
- # Discovers methods of the form +migrate+__attribute_ implemented for the paths
293
- # in the given class => paths hash the given klass. The migrate method is called
294
- # on the input field value corresponding to the path.
295
- def migration_filters(klass, attr_mds)
296
- # the attribute => migration method hash
297
- mth_hash = attribute_method_hash(klass, attr_mds)
298
- proc_hash = attribute_proc_hash(klass, attr_mds)
299
- return if mth_hash.empty? and proc_hash.empty?
300
-
301
- # for each class path terminal attribute metadata, add the migration filters
302
- # to the attribute metadata => filter hash
303
- attr_mds.to_compact_hash do |attr_md|
304
- # the filter proc
305
- proc = proc_hash[attr_md.to_sym]
306
- # the migration shim method
307
- mth = mth_hash[attr_md.to_sym]
308
- if mth then
309
- if proc then
310
- Proc.new do |obj, value, row|
311
- # filter the value
312
- fval = proc.call(value)
313
- # call the migration method on the filtered value
314
- obj.send(mth, fval, row) unless fval.nil?
315
- end
316
- else
317
- # call the migration method
318
- Proc.new { |obj, value, row| obj.send(mth, value, row) }
319
- end
320
- elsif proc then
321
- # call the filter
322
- Proc.new { |obj, value, row| proc.call(value) }
323
- end
324
- end
325
- end
326
-
327
- def attribute_method_hash(klass, attr_mds)
328
- # the migrate methods, excluding the Migratable migrate_references method
329
- mths = klass.instance_methods(true).select { |mth| mth =~ /^migrate.(?!references)/ }
330
- # the attribute => migration method hash
331
- mth_hash = {}
332
- mths.each do |mth|
333
- # the attribute suffix, e.g. name for migrate_name or Name for migrateName
334
- suffix = /^migrate(_)?(.*)/.match(mth).captures[1]
335
- # the attribute name
336
- attr_nm = suffix[0, 1].downcase + suffix[1..-1]
337
- # the attribute for the name, or skip if no such attribute
338
- attr = klass.standard_attribute(attr_nm) rescue next
339
- # associate the attribute => method
340
- mth_hash[attr] = mth
341
- end
342
- mth_hash
343
- end
344
-
345
- # @return [Attribute => {Object => Object}] the filter migration methods
346
- def attribute_proc_hash(klass, attr_mds)
347
- hash = @filter_hash[klass]
348
- if hash.nil? then return Hash::EMPTY_HASH end
349
- proc_hash = {}
350
- attr_mds.each do |attr_md|
351
- flt = hash[attr_md.to_sym] || next
352
- proc_hash[attr_md.to_sym] = to_filter_proc(flt)
353
- end
354
- logger.debug { "Migration filters loaded for #{klass.qp} #{proc_hash.keys.to_series}." }
355
- proc_hash
356
- end
357
-
358
- # Builds a proc that filters the input value. The config filter mapping entry is one of the following:
359
- # * literal: literal
360
- # * regexp: literal
361
- # * regexp: template
362
- #
363
- # The regexp template can include match references (+$1+, +$2+, etc.) corresponding to the regexp captures.
364
- # If the input value equals a literal, then the mapped literal is returned. Otherwise, if the input value
365
- # matches a regexp, then the mapped transformation is returned after reference substitution. Otherwise,
366
- # the input value is returned unchanged.
367
- #
368
- # For example, the config:
369
- # /(\d{1,2})\/x\/(\d{1,2})/: $1/1/$2
370
- # n/a: ~
371
- # converts the input value as follows:
372
- # 3/12/02 => 3/12/02 (no match)
373
- # 5/x/04 => 5/1/04
374
- # n/a => nil
375
- #
376
- # @param [{Object => Object}] filter the config value mapping
377
- # @return [Proc] the filter migration block
378
- # @raise [MigrationError] if the filter includes a regexp option other than +i+ (case-insensitive)
379
- def to_filter_proc(filter)
380
- # Split the filter into a straight value => value hash and a pattern => value hash.
381
- ph, vh = filter.split { |k, v| k =~ REGEXP_PAT }
382
- # The Regexp => value hash is built from the pattern => value hash.
383
- reh = {}
384
- ph.each do |k, v|
385
- # The /pattern/opts string is parsed to the pattern and options.
386
- pat, opt = REGEXP_PAT.match(k).captures
387
- # Convert the regexp i option character to a Regexp initializer parameter.
388
- reopt = if opt then
389
- case opt
390
- when 'i' then Regexp::IGNORECASE
391
- else raise MigrationError.new("Migration value filter regular expression #{k} qualifier not supported: expected 'i', found '#{opt}'")
392
- end
393
- end
394
- # the Regexp object
395
- re = Regexp.new(pat, reopt)
396
- # The regexp value can include match references ($1, $2, etc.). In that case, replace the $
397
- # match reference with a %s print reference, since the filter formats the matching input value.
398
- reh[re] = String === v ? v.gsub(/\$\d/, '%s') : v
399
- end
400
- # The new proc matches preferentially on the literal value, then the first matching regexp.
401
- # If no match on either a literal or a regexp, then the value is preserved.
402
- Proc.new do |value|
403
- if vh.has_key?(value) then
404
- vh[value]
405
- else
406
- # The first regex which matches the value.
407
- regexp = reh.detect_key { |re| value =~ re }
408
- # If there is a match, then apply the filter to the match data.
409
- # Otherwise, pass the value through unmodified.
410
- if regexp then
411
- v = reh[regexp]
412
- String === v ? v % $~.captures : v
413
- else
414
- value
415
- end
416
- end
417
- end
418
- end
419
-
420
- # Loads the shim files.
421
- #
422
- # @param [<String>, String] files the file or file array
423
- def load_shims(files)
424
- logger.debug { "Loading shims with load path #{$:.pp_s}..." }
425
- files.enumerate do |file|
426
- # load the file
427
- begin
428
- require file
429
- rescue Exception => e
430
- logger.error("Migrator couldn't load shim file #{file} - #{e}.")
431
- raise
432
- end
433
- logger.info { "Migrator loaded shim file #{file}." }
434
- end
435
- end
436
-
437
- # Migrates all rows in the input.
438
- #
439
- # @yield (see #migrate)
440
- # @yieldparam (see #migrate)
441
- def migrate_rows
442
- # open an CSV output for bad records if the option is set
443
- if @bad_rec_file then
444
- @loader.trash = @bad_rec_file
445
- logger.info("Unmigrated records will be written to #{File.expand_path(@bad_rec_file)}.")
446
- end
447
- @rec_cnt = mgt_cnt = 0
448
- logger.info { "Migrating #{@input}..." }
449
- @loader.each do |row|
450
- # the one-based current record number
451
- rec_no = @rec_cnt + 1
452
- # skip if the row precedes the offset option
453
- @rec_cnt += 1 && next if @rec_cnt < @offset
454
- begin
455
- # migrate the row
456
- logger.debug { "Migrating record #{rec_no}..." }
457
- target = migrate_row(row)
458
- # call the block on the migrated target
459
- if target then
460
- logger.debug { "Migrator built #{target} with the following content:\n#{target.dump}" }
461
- yield target
462
- end
463
- rescue Exception => e
464
- trace = e.backtrace.join("\n")
465
- logger.error("Migration error on record #{rec_no} - #{e.message}:\n#{trace}")
466
- raise unless @bad_file
467
- end
468
- if target then
469
- # replace the log message below with the commented alternative to detect a memory leak
470
- logger.debug { "Migrated record #{rec_no}." }
471
- #memory_usage = `ps -o rss= -p #{Process.pid}`.to_f / 1024 # in megabytes
472
- #logger.debug { "Migrated rec #{@rec_cnt}; memory usage: #{sprintf("%.1f", memory_usage)} MB." }
473
- if @print_progress then print_progress(mgt_cnt) end
474
- mgt_cnt += 1
475
- # clear the migration state
476
- clear(target)
477
- else
478
- # If there is a bad file then warn, reject and continue. Otherwise, bail.
479
- if @bad_rec_file then
480
- logger.warn("Migration not performed on record #{rec_no}.")
481
- @loader.reject(row)
482
- else
483
- raise MigrationError.new("Migration not performed on record #{rec_no}")
484
- end
485
- end
486
- # Bump the record count.
487
- @rec_cnt += 1
488
- end
489
- logger.info("Migrated #{mgt_cnt} of #{@rec_cnt} records.")
490
- end
491
-
492
- # Prints a +\++ progress indicator to stdout if the count parameter is divisible by ten.
493
- #
494
- # @param [Integer] count the progress step count
495
- def print_progress(count)
496
- if count % 720 then puts end
497
- if count % 10 == 0 then puts "+" else print "+" end
498
- end
499
-
500
- # Clears references to objects allocated for migration of a single row into the given target.
501
- # This method does nothing. Subclasses can override.
502
- #
503
- # This method is overridden by subclasses to clear the migration state to conserve memory,
504
- # since this migrator should consume O(1) rather than O(n) memory for n migration records.
505
- def clear(target)
506
- end
507
-
508
- # Imports the given CSV row into a target object.
509
- #
510
- # @param [{Symbol => Object}] row the input row field => value hash
511
- # @return the migrated target object if the migration is valid, nil otherwise
512
- def migrate_row(row)
513
- # create an instance for each creatable class
514
- created = Set.new
515
- migrated = @creatable_classes.map { |klass| create(klass, row, created) }
516
- # migrate each object from the input row
517
- created.each { |obj| obj.migrate(row, migrated) }
518
- valid = migrate_valid_references(row, migrated)
519
- # the target object
520
- target = valid.detect { |obj| @target_class === obj } || return
521
- logger.debug { "Migrated target #{target}." }
522
- target
523
- end
524
-
525
- # Sets the migration references for each valid migrated object.
526
- #
527
- # @param [Array] the migrated objects
528
- # @return [Array] the valid migrated objects
529
- def migrate_valid_references(row, migrated)
530
- # Split the valid and invalid objects. The iteration is in reverse dependency order,
531
- # since invalidating a dependent can invalidate the owner.
532
- valid, invalid = migrated.transitive_closure(:dependents).reverse.partition do |obj|
533
- if migration_valid?(obj) then
534
- obj.migrate_references(row, migrated, @attr_flt_hash[obj.class])
535
- true
536
- else
537
- obj.class.owner_attributes.each { |attr| obj.clear_attribute(attr) }
538
- false
539
- end
540
- end
541
-
542
- # Go back through the valid objects in dependency order to invalidate dependents
543
- # whose owner is invalid.
544
- valid.reverse.each do |obj|
545
- unless owner_valid?(obj, valid, invalid) then
546
- invalid << valid.delete(obj)
547
- logger.debug { "Invalidated migrated #{obj} since it does not have a valid owner." }
548
- end
549
- end
550
-
551
- # Go back through the valid objects in reverse dependency order to invalidate owners
552
- # created only to hold a dependent which was subsequently invalidated.
553
- valid.reject do |obj|
554
- if @owners.include?(obj.class) and obj.dependents.all? { |dep| invalid.include?(dep) } then
555
- # clear all references from the invalidated owner
556
- obj.class.domain_attributes.each_metadata { |attr_md| obj.clear_attribute(attr_md.to_sym) }
557
- invalid << obj
558
- logger.debug { "Invalidated #{obj.qp} since it was created solely to hold subsequently invalidated dependents." }
559
- true
560
- end
561
- end
562
- end
563
-
564
- # Returns whether the given domain object satisfies at least one of the following conditions:
565
- # * it does not have an owner among the invalid objects
566
- # * it has an owner among the valid objects
567
- #
568
- # @param [Resource] obj the domain object to check
569
- # @param [<Resource>] valid the valid migrated objects
570
- # @param [<Resource>] invalid the invalid migrated objects
571
- # @return [Boolean] whether the owner is valid
572
- def owner_valid?(obj, valid, invalid)
573
- otypes = obj.class.owners
574
- invalid.all? { |other| not otypes.include?(other.class) } or
575
- valid.any? { |other| otypes.include?(other.class) }
576
- end
577
-
578
- # @param [Migratable] obj the migrated object
579
- # @return [Boolean] whether the migration is successful
580
- def migration_valid?(obj)
581
- if obj.migration_valid? then
582
- true
583
- else
584
- logger.debug { "Migrated #{obj.qp} is invalid." }
585
- false
586
- end
587
- end
588
-
589
- # Creates an instance of the given klass from the given row.
590
- # The new klass instance and all intermediate migrated instances are added to the
591
- # created set.
592
- #
593
- # @param [Class] klass
594
- # @param [{Symbol => Object}] row the input row
595
- # @param [<Resource>] created the migrated instances for this row
596
- # @return [Resource] the new instance
597
- def create(klass, row, created)
598
- # the new object
599
- logger.debug { "Migrator building #{klass.qp}..." }
600
- created << obj = klass.new
601
- migrate_attributes(obj, row, created)
602
- add_defaults(obj, row, created)
603
- logger.debug { "Migrator built #{obj}." }
604
- obj
605
- end
606
-
607
- # @param [Resource] the migration object
608
- # @param row (see #create)
609
- # @param [<Resource>] created (see #create)
610
- def migrate_attributes(obj, row, created)
611
- # for each input header which maps to a migratable target attribute metadata path,
612
- # set the target attribute, creating intermediate objects as needed.
613
- @cls_paths_hash[obj.class].each do |path|
614
- header = @header_map[path][obj.class]
615
- # the input value
616
- value = row[header]
617
- next if value.nil?
618
- # fill the reference path
619
- ref = fill_path(obj, path[0...-1], row, created)
620
- # set the attribute
621
- migrate_attribute(ref, path.last, value, row)
622
- end
623
- end
624
-
625
- # @param [Resource] the migration object
626
- # @param row (see #create)
627
- # @param [<Resource>] created (see #create)
628
- def add_defaults(obj, row, created)
629
- dh = @def_hash[obj.class] || return
630
- dh.each do |path, value|
631
- # fill the reference path
632
- ref = fill_path(obj, path[0...-1], row, created)
633
- # set the attribute to the default value unless there is already a value
634
- ref.merge_attribute(path.last.to_sym, value)
635
- end
636
- end
637
-
638
- # Fills the given reference Attribute path starting at obj.
639
- #
640
- # @param row (see #create)
641
- # @param created (see #create)
642
- # @return the last domain object in the path
643
- def fill_path(obj, path, row, created)
644
- # create the intermediate objects as needed (or return obj if path is empty)
645
- path.inject(obj) do |parent, attr_md|
646
- # the referenced object
647
- parent.send(attr_md.reader) or create_reference(parent, attr_md, row, created)
648
- end
649
- end
650
-
651
- # Sets the given migrated object's reference attribute to a new referenced domain object.
652
- #
653
- # @param [Resource] obj the domain object being migrated
654
- # @param [Attribute] attr_md the attribute being migrated
655
- # @param row (see #create)
656
- # @param created (see #create)
657
- # @return the new object
658
- def create_reference(obj, attr_md, row, created)
659
- if attr_md.type.abstract? then
660
- raise MigrationError.new("Cannot create #{obj.qp} #{attr_md} with abstract type #{attr_md.type}")
661
- end
662
- ref = attr_md.type.new
663
- ref.migrate(row, Array::EMPTY_ARRAY)
664
- obj.send(attr_md.writer, ref)
665
- created << ref
666
- logger.debug { "Migrator created #{obj.qp} #{attr_md} #{ref}." }
667
- ref
668
- end
669
-
670
- # Sets the given attribute value to the filtered input value. If there is a filter
671
- # defined for the attribute, then that filter is applied. If there is a migration
672
- # shim method with name +migrate_+_attribute_, then than method is called on the
673
- # (possibly filtered) value. The target object attribute is set to the resulting
674
- # filtered value.
675
- #
676
- # @param [Migratable] obj the target domain object
677
- # @param [Attribute] attr_md the target attribute
678
- # @param value the input value
679
- # @param [{Symbol => Object}] row the input row
680
- def migrate_attribute(obj, attr_md, value, row)
681
- # if there is a shim migrate_<attribute> method, then call it on the input value
682
- value = filter_value(obj, attr_md, value, row) || return
683
- # set the attribute
684
- begin
685
- obj.send(attr_md.writer, value)
686
- rescue Exception
687
- raise MigrationError.new("Could not set #{obj.qp} #{attr_md} to #{value.qp} - #{$!}")
688
- end
689
- logger.debug { "Migrated #{obj.qp} #{attr_md} to #{value}." }
690
- end
691
-
692
- # Calls the shim migrate_<attribute> method or config filter on the input value.
693
- #
694
- # @param value the input value
695
- # @return the input value, if there is no filter, otherwise the filtered value
696
- def filter_value(obj, attr_md, value, row)
697
- filter = filter_for(obj, attr_md)
698
- return value if filter.nil?
699
- fval = filter.call(obj, value, row)
700
- unless value == fval then
701
- logger.debug { "Migration filter transformed #{obj.qp} #{attr_md} value from #{value.qp} to #{fval.qp}." }
702
- end
703
- fval
704
- end
705
-
706
- def filter_for(obj, attr_md)
707
- flts = @attr_flt_hash[obj.class] || return
708
- flts[attr_md]
709
- end
710
-
711
- # @param [Resource] obj the domain object to save in the database
712
- # @return [Resource, nil] obj if the save is successful, nil otherwise
713
- def save(obj, database)
714
- if @create then
715
- if database.find(obj) then
716
- logger.debug { "Migrator ignored record #{current_record}, since it already exists as #{obj.printable_content(obj.class.secondary_key_attributes)} with id #{obj.identifier}." }
717
- else
718
- logger.debug { "Migrator creating #{obj}..." }
719
- database.create(obj)
720
- logger.debug { "Migrator creating #{obj}." }
721
- end
722
- else
723
- logger.debug { "Migrator saving #{obj}..." }
724
- database.save(obj)
725
- logger.debug { "Migrator saved #{obj}." }
726
- end
727
- end
728
-
729
- def current_record
730
- @rec_cnt + 1
731
- end
732
-
733
- # @param [<String>, String] files the migration fields mapping file or file array
734
- # @return [{Class => {Attribute => Symbol}}] the class => path => header hash
735
- # loaded from the mapping files
736
- def load_field_map_files(files)
737
- map = LazyHash.new { Hash.new }
738
- files.enumerate { |file| load_field_map_file(file, map) }
739
-
740
- # include the target class
741
- map[@target_class] ||= Hash.new
742
- # include the default classes
743
- @def_hash.each_key { |klass| map[klass] ||= Hash.new }
744
-
745
- # add superclass paths into subclass paths
746
- map.each do |klass, path_hdr_hash|
747
- map.each do |other, other_path_hdr_hash|
748
- if klass < other then
749
- # add, but don't replace, path => header entries from superclass
750
- path_hdr_hash.merge!(other_path_hdr_hash) { |key, old, new| old }
751
- end
752
- end
753
- end
754
-
755
- # include only concrete classes
756
- classes = map.keys
757
- map.delete_if do |klass, paths|
758
- klass.abstract? or classes.any? { |other| other < klass }
759
- end
760
-
761
- map
762
- end
763
-
764
- # @param [String] file the migration fields configuration file
765
- # @param [{Class => {Attribute => Symbol}}] hash the class => path => header hash
766
- # loaded from the configuration file
767
- def load_field_map_file(file, hash)
768
- # load the field mapping config file
769
- begin
770
- config = YAML::load_file(file)
771
- rescue
772
- raise MigrationError.new("Could not read field map file #{file}: " + $!)
773
- end
774
-
775
- # collect the class => path => header entries
776
- config.each do |field, attr_list|
777
- next if attr_list.blank?
778
- # the header accessor method for the field
779
- header = @loader.accessor(field)
780
- raise MigrationError.new("Field defined in migration configuration #{file} not found in input file #{@input} headers: #{field}") if header.nil?
781
- # associate each attribute path in the property value with the header
782
- attr_list.split(/,\s*/).each do |path_s|
783
- klass, path = create_attribute_path(path_s)
784
- hash[klass][path] = header
785
- end
786
- end
787
- end
788
-
789
- # Loads the defaults config files.
790
- #
791
- # @param [<String>, String] files the file or file array to load
792
- # @return [<Class => <String => Object>>] the class => path => default value entries
793
- def load_defaults_files(files)
794
- # collect the class => path => value entries from each defaults file
795
- hash = LazyHash.new { Hash.new }
796
- files.enumerate { |file| load_defaults_file(file, hash) }
797
- hash
798
- end
799
-
800
- # Loads the defaults config file into the given hash.
801
- #
802
- # @param [String] file the file to load
803
- # @param [<Class => <String => Object>>] hash the class => path => default value entries
804
- def load_defaults_file(file, hash)
805
- begin
806
- config = YAML::load_file(file)
807
- rescue
808
- raise MigrationError.new("Could not read defaults file #{file}: " + $!)
809
- end
810
- # collect the class => path => value entries
811
- config.each do |path_s, value|
812
- next if value.nil_or_empty?
813
- klass, path = create_attribute_path(path_s)
814
- hash[klass][path] = value
815
- end
816
- end
817
- # Loads the filter config files.
818
- #
819
- # @param [<String>, String] files the file or file array to load
820
- # @return [<Class => <String => Object>>] the class => path => default value entries
821
- def load_filter_files(files)
822
- # collect the class => path => value entries from each defaults file
823
- hash = {}
824
- files.enumerate { |file| load_filter_file(file, hash) }
825
- hash
826
- end
827
-
828
- # Loads the filter config file into the given hash.
829
- #
830
- # @param [String] file the file to load
831
- # @param [<Class => <String => <Object => Object>>>] hash the class => path => input value => caTissue value entries
832
- def load_filter_file(file, hash)
833
- begin
834
- config = YAML::load_file(file)
835
- rescue
836
- raise MigrationError.new("Could not read filter file #{file}: " + $!)
837
- end
838
- # collect the class => attribute => filter entries
839
- config.each do |path_s, flt|
840
- next if flt.nil_or_empty?
841
- klass, path = create_attribute_path(path_s)
842
- unless path.size == 1 then
843
- raise MigrationError.new("Migration filter configuration path not supported: #{path_s}")
844
- end
845
- attr = klass.standard_attribute(path.first.to_sym)
846
- flt_hash = hash[klass] ||= {}
847
- flt_hash[attr] = flt
848
- end
849
- end
850
-
851
- # @param [String] path_s a period-delimited path string path_s in the form _class_(._attribute_)+
852
- # @return [<Attribute>] the corresponding attribute metadata path
853
- # @raise [MigrationError] if the path string is malformed or an attribute is not found
854
- def create_attribute_path(path_s)
855
- names = path_s.split('.')
856
- # if the path starts with a capitalized class name, then resolve the class.
857
- # otherwise, the target class is the start of the path.
858
- klass = names.first =~ /^[A-Z]/ ? class_for_name(names.shift) : @target_class
859
- # there must be at least one attribute
860
- if names.empty? then
861
- raise MigrationError.new("Attribute entry in migration configuration is not in <class>.<attribute> format: #{value}")
862
- end
863
- # build the Attribute path
864
- path = []
865
- names.inject(klass) do |parent, name|
866
- attr = name.to_sym
867
- attr_md = begin
868
- parent.attribute_metadata(attr)
869
- rescue NameError
870
- raise MigrationError.new("Migration field mapping attribute #{parent.qp}.#{attr} not found: #{$!}")
871
- end
872
- if attr_md.collection? then
873
- raise MigrationError.new("Migration field mapping attribute #{parent.qp}.#{attr} is a collection, which is not supported")
874
- end
875
- path << attr_md
876
- attr_md.type
877
- end
878
- # return the starting class and Attribute path.
879
- # note that the starting class is not necessarily the first path attribute declarer, since the
880
- # starting class could be the concrete target class rather than an abstract declarer. this is
881
- # important, since the class must be instantiated.
882
- [klass, path]
883
- end
884
-
885
- # @param [String] name the class name, without the {#context_module}
886
- # @return [Class] the corresponding class
887
- def class_for_name(name)
888
- # navigate through the scope to the final class
889
- name.split('::').inject(context_module) do |ctxt, cnm|
890
- ctxt.const_get(cnm)
891
- end
892
- end
893
-
894
- # The context module is given by the target class {ResourceClass#domain_module}.
895
- #
896
- # @return [Module] the class name resolution context
897
- def context_module
898
- @target_class.domain_module
899
- end
900
-
901
- # @return a new class => [paths] hash from the migration fields configuration map
902
- def create_class_paths_hash(fld_map, def_map)
903
- hash = {}
904
- fld_map.each { |klass, path_hdr_hash| hash[klass] = path_hdr_hash.keys.to_set }
905
- def_map.each { |klass, path_val_hash| (hash[klass] ||= Set.new).merge(path_val_hash.keys) }
906
- hash
907
- end
908
-
909
- # @return a new path => class => header hash from the migration fields configuration map
910
- def create_header_map(fld_map)
911
- hash = LazyHash.new { Hash.new }
912
- fld_map.each do |klass, path_hdr_hash|
913
- path_hdr_hash.each { |path, hdr| hash[path][klass] = hdr }
914
- end
915
- hash
916
- end
917
- end
918
- end