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,533 @@
1
+ # load the required gems
2
+ require 'rubygems'
3
+
4
+ # the UOM 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/resource'
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 target application {CaRuby::Database}
31
+ # @option opts [String] :target required target domain class
32
+ # @option opts [String] :input required source file to migrate
33
+ # @option opts [String] :shims optional array of shim files to load
34
+ # @option opts [String] :bad write each invalid record to the given file and continue migration
35
+ # @option opts [String] :offset zero-based starting source record number to process (default 0)
36
+ def initialize(opts)
37
+ parse_options(opts)
38
+ build
39
+ end
40
+
41
+ # Imports this migrator's file into the database with the given connect options.
42
+ # This method creates or updates the domain objects mapped from the import source.
43
+ # If a block is given to this method, then the block is called on each stored
44
+ # migration target object.
45
+ #
46
+ # @yield [target] operation performed on the migration target
47
+ # @yieldparam [Resource] target the migrated target domain object
48
+ def migrate_to_database(&block)
49
+ # migrate with save
50
+ tm = Stopwatch.measure { execute_save(&block) }.elapsed
51
+ logger.debug { format_migration_time_log_message(tm) }
52
+ end
53
+
54
+ # Imports this migrator's CSV file and calls the required block on each migrated target
55
+ # domain object.
56
+ #
57
+ # @yield [target] operation performed on the migration target
58
+ # @yieldparam [Resource] target the migrated target domain object
59
+ def migrate(&block)
60
+ raise MigrationError.new("No migration block") unless block_given?
61
+ migrate_rows(&block)
62
+ end
63
+
64
+ alias :each :migrate
65
+
66
+ private
67
+
68
+ # Class {#migrate} with a {#save} block.
69
+ def execute_save
70
+ @database.open do |db|
71
+ migrate do |target|
72
+ save(target, db)
73
+ yield target if block_given?
74
+ end
75
+ end
76
+ end
77
+
78
+ # @return a log message String for the given migration time in seconds
79
+ def format_migration_time_log_message(time)
80
+ # the database execution time
81
+ dt = @database.execution_time
82
+ if time > 120 then
83
+ time /= 60
84
+ dt /= 60
85
+ unit = "minutes"
86
+ else
87
+ unit = "seconds"
88
+ end
89
+ "Migration took #{'%.2f' % time} #{unit}, of which #{'%.2f' % dt} were database operations."
90
+ end
91
+
92
+ def parse_options(opts)
93
+ @fld_map_file = opts[:mapping]
94
+ raise MigrationError.new("Migrator missing required field mapping file parameter") if @fld_map_file.nil?
95
+ @shims = opts[:shims] ||= []
96
+ @offset = opts[:offset] ||= 0
97
+ @input = Options.get(:input, opts)
98
+ raise MigrationError.new("Migrator missing required source file parameter") if @input.nil?
99
+ @database = Options.get(:database, opts)
100
+ raise MigrationError.new("Migrator missing required database parameter") if @database.nil?
101
+ @target_class = Options.get(:target, opts)
102
+ raise MigrationError.new("Migrator missing required target class parameter") if @target_class.nil?
103
+ @bad_rec_file = opts[:bad]
104
+ logger.info("Migration options: #{opts.reject { |option, value| value.nil_or_empty? }.pp_s}.")
105
+ end
106
+
107
+ def build
108
+ # the current source class => instance map
109
+ raise MigrationError.new("No file to migrate") if @input.nil?
110
+
111
+ # make a CSV loader which only converts input fields corresponding to non-String attributes
112
+ logger.info { "Migration input file: #{@input}." }
113
+ @loader = CsvIO.new(@input) do |value, info|
114
+ value unless @nonstring_headers.include?(info.header)
115
+ end
116
+
117
+ # create the class => path => header hash
118
+ fld_map = load_field_map(@fld_map_file)
119
+ # create the class => paths hash
120
+ @cls_paths_hash = create_class_paths_hash(fld_map)
121
+ # create the path => class => header hash
122
+ @header_map = create_header_map(fld_map)
123
+ # add missing owner classes (copy the keys rather than using each_key since the hash is updated)
124
+ @cls_paths_hash.keys.each { |klass| add_owners(klass) }
125
+ # order the creatable classes by dependency, owners first, to smooth the migration process
126
+ @creatable_classes = @cls_paths_hash.keys.sort! { |klass, other| other.depends_on?(klass) ? -1 : (klass.depends_on?(other) ? 1 : 0) }
127
+ # print the maps
128
+ print_hash = LazyHash.new { Hash.new }
129
+ @cls_paths_hash.each do |klass, paths|
130
+ print_hash[klass.qp] = paths.map { |path| {path.map { |attr_md| attr_md.to_sym }.join('.') => @header_map[path][klass] } }
131
+ end
132
+ logger.info { "Migration paths:\n#{print_hash.pp_s}" }
133
+ logger.info { "Migration creatable classes: #{@creatable_classes.qp}." }
134
+
135
+ # add shim modifiers
136
+ load_shims(@shims)
137
+
138
+ # the class => attribute migration methods hash
139
+ create_migration_method_hashes
140
+
141
+ # collect the String input fields for the custom CSVLoader converter
142
+ @nonstring_headers = Set.new
143
+ logger.info("Migration attributes:")
144
+ @header_map.each do |path, cls_hdr_hash|
145
+ attr_md = path.last
146
+ cls_hdr_hash.each do |klass, hdr|
147
+ type_s = attr_md.type ? attr_md.type.qp : 'Object'
148
+ logger.info(" #{hdr} => #{klass.qp}.#{path.join('.')} (#{type_s})")
149
+ end
150
+ @nonstring_headers.merge!(cls_hdr_hash.values) if attr_md.type != Java::JavaLang::String
151
+ end
152
+ end
153
+
154
+ # Adds missing klass owner classes to the migration class path hash (with empty paths).
155
+ def add_owners(klass)
156
+ klass.owners.each do |owner|
157
+ next if @cls_paths_hash.detect_key { |other| other <= owner } or owner.abstract?
158
+ logger.debug { "Migrator adding #{klass.qp} owner #{owner.qp}" }
159
+ @cls_paths_hash[owner] = Array::EMPTY_ARRAY
160
+ add_owners(owner)
161
+ end
162
+ end
163
+
164
+ # Creates the class => +migrate_+_<attribute>_ hash for the given klasses.
165
+ def create_migration_method_hashes
166
+ # the attribute metadata => migration method hash variable
167
+ @attr_md_mgt_mth_map = {}
168
+ # the class => attribute => migration method hash variable
169
+ @mgt_mth_hash = {}
170
+ # collect the migration methods
171
+ customizable_class_attributes.each { |klass, attr_mds| add_migration_methods(klass, attr_mds) }
172
+ # print the migration shim methods
173
+ unless @mgt_mth_hash.empty? then
174
+ printer_hash = LazyHash.new { Array.new }
175
+ @mgt_mth_hash.each do |klass, attr_mth_hash|
176
+ mthds = attr_mth_hash.values
177
+ printer_hash[klass.qp] = mthds unless mthds.empty?
178
+ end
179
+ logger.info("Migration shim methods: #{printer_hash.pp_s}.")
180
+ end
181
+ end
182
+
183
+ # @return the class => attributes hash for terminal path attributes which can be customized by +migrate_+ methods
184
+ def customizable_class_attributes
185
+ # The customizable classes set, starting with creatable classes and adding in
186
+ # the migration path terminal attribute declarer classes below.
187
+ klasses = @creatable_classes.to_set
188
+ # the class => path terminal attributes hash
189
+ cls_attrs_hash = LazyHash.new { Set.new }
190
+ # add each path terminal attribute and its declarer class
191
+ @cls_paths_hash.each_value do |paths|
192
+ paths.each do |path|
193
+ attr_md = path.last
194
+ type = attr_md.declarer
195
+ klasses << type
196
+ cls_attrs_hash[type] << attr_md
197
+ end
198
+ end
199
+
200
+ # Merge each redundant customizable superclass into its concrete customizable subclasses.
201
+ klasses.dup.each do |cls|
202
+ redundant = false
203
+ klasses.each do |other|
204
+ # cls is redundant if it is a superclass of other
205
+ redundant = other < cls
206
+ if redundant then
207
+ cls_attrs_hash[other].merge!(cls_attrs_hash[cls])
208
+ end
209
+ end
210
+ # remove the redundant class
211
+ if redundant then
212
+ cls_attrs_hash.delete(cls)
213
+ klasses.delete(cls)
214
+ end
215
+ end
216
+
217
+ cls_attrs_hash
218
+ end
219
+
220
+ # Discovers methods of the form +migrate+__attribute_ implemented for the paths
221
+ # in the given class => paths hash the given klass. The migrate method is called
222
+ # on the input field value corresponding to the path.
223
+ def add_migration_methods(klass, attr_mds)
224
+ # the migrate methods, excluding the Migratable migrate_references method
225
+ mths = klass.instance_methods(true).select { |mth| mth =~ /^migrate.(?!references)/ }
226
+ return if mths.empty?
227
+
228
+ # the attribute => migration method hash
229
+ attr_mth_hash = {}
230
+ mths.each do |mth|
231
+ # the attribute suffix, e.g. name for migrate_name or Name for migrateName
232
+ suffix = /^migrate(_)?(.*)/.match(mth).captures[1]
233
+ # the attribute name
234
+ attr_nm = suffix[0, 1].downcase + suffix[1..-1]
235
+ # the attribute for the name, or skip if no such attribute
236
+ attr = klass.standard_attribute(attr_nm) rescue next
237
+ # associate the attribute => method
238
+ attr_mth_hash[attr] = mth
239
+ end
240
+
241
+ # for each class path terminal attribute metadata, add the migration methods
242
+ # to the attribute metadata => migration method hash
243
+ attr_mds.each do |attr_md|
244
+ # the attribute migration method
245
+ mth = attr_mth_hash[attr_md.to_sym]
246
+ # associate the AttributeMetadata => method
247
+ @attr_md_mgt_mth_map[attr_md] ||= mth if mth
248
+ end
249
+ @mgt_mth_hash[klass] = attr_mth_hash
250
+ end
251
+
252
+ # loads the shim files.
253
+ def load_shims(files)
254
+ logger.debug { "Loading shims with load path #{$:.pp_s}..." }
255
+ files.each do |file|
256
+ # load the file
257
+ begin
258
+ require file
259
+ rescue Exception => e
260
+ logger.error("Migrator couldn't load shim file #{file} - #{e}.")
261
+ raise
262
+ end
263
+ logger.info { "Migrator loaded shim file #{file}." }
264
+ end
265
+ end
266
+
267
+ # Migrates all rows in the input.
268
+ # The required block to this method is described in {#migrate}.
269
+ def migrate_rows # :yields: target
270
+ # open an CSV output for bad records if the option is set
271
+ if @bad_rec_file then
272
+ @loader.trash = @bad_rec_file
273
+ logger.info("Unmigrated records will be written to #{File.expand_path(@bad_rec_file)}.")
274
+ end
275
+ rec_cnt = mgt_cnt = 0
276
+ logger.info { "Migrating #{@input}..." }
277
+ @loader.each do |row|
278
+ # the one-based current record number
279
+ rec_no = rec_cnt + 1
280
+ # skip if the row precedes the offset option
281
+ rec_cnt += 1 && next if rec_cnt < @offset
282
+ begin
283
+ # migrate the row
284
+ target = migrate_row(row)
285
+ # call the block on the migrated target
286
+ if target then
287
+ logger.debug { "Migrator built #{target} with the following content:\n#{target.dump}" }
288
+ yield target
289
+ end
290
+ rescue Exception => e
291
+ trace = e.backtrace.join("\n")
292
+ logger.error("Migration error on record #{rec_no} - #{e.message}:\n#{trace}")
293
+ raise unless @bad_file
294
+ end
295
+ if target then
296
+ # replace the log message below with the commented alternative to detect a memory leak
297
+ logger.debug { "Migrated record #{rec_no}." }
298
+ #memory_usage = `ps -o rss= -p #{Process.pid}`.to_f / 1024 # in megabytes
299
+ #logger.debug { "Migrated rec #{@rec_cnt}; memory usage: #{sprintf("%.1f", memory_usage)} MB." }
300
+ mgt_cnt += 1
301
+ # clear the migration state
302
+ clear(target)
303
+ else
304
+ # If there is a bad file then warn, reject and continue.
305
+ # Otherwise, bail.
306
+ if @bad_rec_file then
307
+ logger.warn("Migration not performed on record #{rec_no}.")
308
+ @loader.reject(row)
309
+ else
310
+ raise MigrationError.new("Migration not performed on record #{rec_no}.")
311
+ end
312
+ end
313
+ rec_cnt += 1
314
+ end
315
+ logger.info("Migrated #{mgt_cnt} of #{rec_cnt} records.")
316
+ end
317
+
318
+ # Clears references to objects allocated for migration of a single row into the given target.
319
+ # This method does nothing. Subclasses can override.
320
+ #
321
+ # This method is overridden by subclasses to clear the migration state to conserve memory,
322
+ # since this migrator should consume O(1) rather than O(n) memory for n migration records.
323
+ def clear(target)
324
+ end
325
+
326
+ # Imports the given CSV row into a target object.
327
+ #
328
+ # @param [{Symbol => Object}] row the input row field => value hash
329
+ # @return the migrated target object if the migration is valid, nil otherwise
330
+ def migrate_row(row) # :yields: target
331
+ # create an instance for each creatable class
332
+ created = Set.new
333
+ migrated = @creatable_classes.map { |klass| create(klass, row, created) }
334
+ # migrate each object from the input row
335
+ created.each { |obj| obj.migrate(row, migrated) }
336
+ # set the references
337
+ migrated.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
338
+ # remove invalid migrations
339
+ migrated.delete_if { |obj| not migration_valid?(obj) }
340
+ # the target object
341
+ target = migrated.detect { |obj| @target_class === obj }
342
+ if target then
343
+ logger.debug { "Migrated target #{target}." }
344
+ target
345
+ end
346
+ end
347
+
348
+ # @param [Migratable] obj the migrated object
349
+ # @return whether the migration is successful
350
+ def migration_valid?(obj)
351
+ if obj.migration_valid? then
352
+ true
353
+ else
354
+ logger.debug { "Migrated #{obj.qp} is invalid." }
355
+ false
356
+ end
357
+ end
358
+
359
+ # Creates an instance of the given klass from the given row.
360
+ # The new klass instance and all intermediate migrated instances are added to the
361
+ # created set.
362
+ #
363
+ # @param [Class] klass
364
+ # @param [{Symbol => Object}] row the input row
365
+ # @param [<Resource>] the migrated instances for this row
366
+ # @return the new klass instance
367
+ def create(klass, row, created)
368
+ # the new object
369
+ created << obj = klass.new
370
+ # for each input header which maps to a migratable target attribute metadata path,
371
+ # set the target attribute, creating intermediate objects as needed.
372
+ @cls_paths_hash[klass].each do |path|
373
+ header = @header_map[path][klass]
374
+ # the input value
375
+ value = row[header]
376
+ next if value.nil?
377
+ # fill the reference path
378
+ ref = fill_path(obj, path[0...-1], row, created)
379
+ # set the attribute
380
+ migrate_attribute(ref, path.last, value, row)
381
+ end
382
+ logger.debug { "Migrator created #{obj}." }
383
+ obj
384
+ end
385
+
386
+ # Fills the given reference AttributeMetadata path starting at obj.
387
+ #
388
+ # @param row (see #create)
389
+ # @param created (see #create)
390
+ # @return the last domain object in the path
391
+ def fill_path(obj, path, row, created)
392
+ # create the intermediate objects as needed (or return obj if path is empty)
393
+ path.inject(obj) do |parent, attr_md|
394
+ # the referenced object
395
+ parent.send(attr_md.reader) or create_reference(parent, attr_md, row, created)
396
+ end
397
+ end
398
+
399
+ # Sets the given parent reference AttributeMetadata attr_md attribute to a new domain object.
400
+ #
401
+ # @param [Resource] parent the domain object being migrated
402
+ # @param [AttributeMetadata] attr_md the attribute being migrated
403
+ # @param row (see #create)
404
+ # @param created (see #create)
405
+ # @return the new object
406
+ def create_reference(parent, attr_md, row, created)
407
+ if attr_md.type.abstract? then
408
+ raise MigrationError.new("Cannot create #{parent.qp} #{attr_md} with abstract type #{attr_md.type}")
409
+ end
410
+ ref = attr_md.type.new
411
+ ref.migrate(row, Array::EMPTY_ARRAY)
412
+ parent.send(attr_md.writer, ref)
413
+ created << ref
414
+ logger.debug { "Migrator created #{parent.qp} #{attr_md} #{ref}." }
415
+ ref
416
+ end
417
+
418
+ # Sets the obj migratable AttributeMetadata attr_md to value from the given input row.
419
+ def migrate_attribute(obj, attr_md, value, row)
420
+ # a single value can be used for both a Numeric and a String attribute; coerce the value if necessary
421
+ # if there is a shim migrate_<attribute> method, then call it on the input value
422
+ mth = @attr_md_mgt_mth_map[attr_md]
423
+ if mth and obj.respond_to?(mth) then
424
+ value = obj.send(mth, value, row)
425
+ return if value.nil?
426
+ end
427
+ # set the attribute
428
+ begin
429
+ obj.send(attr_md.writer, value)
430
+ rescue Exception => e
431
+ raise MigrationError.new("Could not set #{obj.qp} #{attr_md} to #{value.qp} - #{e}")
432
+ end
433
+ logger.debug { "Migrated #{obj.qp} #{attr_md} to #{value}." }
434
+ end
435
+
436
+ # @param [Resource] obj the domain object to save in the database
437
+ # @return [Resource, nil] obj if the save is successful, nil otherwise
438
+ def save(obj, database)
439
+ logger.debug { "Migrator saving #{obj}..." }
440
+ database.create(obj)
441
+ logger.debug { "Migrator saved #{obj}." }
442
+ end
443
+
444
+ # @param [String] file the migration fields configuration file
445
+ # @return [{Class => {AttributeMetadata => Symbol}}] the class => path => header hash
446
+ # loaded from the configuration file
447
+ def load_field_map(file)
448
+ # load the field mapping config file
449
+ begin
450
+ config = YAML::load_file(file)
451
+ rescue
452
+ raise MigrationError.new("Could not read field map file #{file}: " + $!)
453
+ end
454
+
455
+ # collect the class => path => header entries
456
+ map = LazyHash.new { Hash.new }
457
+ config.each do |field, attr_list|
458
+ next if attr_list.blank?
459
+ # the header accessor method for the field
460
+ header = @loader.accessor(field)
461
+ raise MigrationError.new("Field defined in migration configuration not found: #{field}") if header.nil?
462
+ attr_list.split(/,\s*/).each do |path_s|
463
+ klass, path = create_attribute_path(path_s)
464
+ map[klass][path] = header
465
+ end
466
+ end
467
+
468
+ # include the target class
469
+ map[@target_class] ||= Hash.new
470
+
471
+ # add superclass paths into subclass paths
472
+ map.each do |klass, path_hdr_hash|
473
+ map.each do |other, other_path_hdr_hash|
474
+ if klass < other then
475
+ # add, but don't replace, path => header entries from superclass
476
+ path_hdr_hash.merge!(other_path_hdr_hash) { |key, old, new| old }
477
+ end
478
+ end
479
+ end
480
+
481
+ # include only concrete classes
482
+ classes = map.enum_keys
483
+ map.delete_if do |klass, paths|
484
+ klass.abstract? or classes.any? { |other| other < klass }
485
+ end
486
+ map
487
+ end
488
+
489
+ # Returns an array of AttributeMetadata objects for the period-delimited path string path_s in the
490
+ # form _class_(._attribute_)+.
491
+ #
492
+ # Raises MigrationError if the path string is malformed or an attribute is not found.
493
+ def create_attribute_path(path_s)
494
+ names = path_s.split('.')
495
+ # if the path starts with a capitalized class name, then resolve the class.
496
+ # otherwise, the target class is the start of the path.
497
+ klass = names.first =~ /^[A-Z]/ ? @target_class.domain_module.const_get(names.shift) : @target_class
498
+ # there must be at least one attribute
499
+ if names.empty? then
500
+ raise MigrationError.new("Attribute entry in migration configuration is not in <class>.<attribute> format: #{value}")
501
+ end
502
+ # build the AttributeMetadata path
503
+ path = []
504
+ names.inject(klass) do |parent, name|
505
+ attr_md = parent.attribute_metadata(name.to_sym) rescue nil
506
+ raise MigrationError.new("Migration field mapping attribute not found: #{parent.qp}.#{name}") if attr_md.nil?
507
+ path << attr_md
508
+ attr_md.type
509
+ end
510
+ # return the starting class and AttributeMetadata path.
511
+ # note that the starting class is not necessarily the first path attribute declarer, since the
512
+ # starting class could be the concrete target class rather than an abstract declarer. this is
513
+ # important, since the class must be instantiated.
514
+ [klass, path]
515
+ end
516
+
517
+ # @return a new class => [paths] hash from the migration fields configuration map
518
+ def create_class_paths_hash(fld_map)
519
+ hash = {}
520
+ fld_map.each { |klass, path_hdr_hash| hash[klass] = path_hdr_hash.keys.to_set }
521
+ hash
522
+ end
523
+
524
+ # @return a new path => class => header hash from the migration fields configuration map
525
+ def create_header_map(fld_map)
526
+ hash = LazyHash.new { Hash.new }
527
+ fld_map.each do |klass, path_hdr_hash|
528
+ path_hdr_hash.each { |path, hdr| hash[path][klass] = hdr }
529
+ end
530
+ hash
531
+ end
532
+ end
533
+ end