caruby-core 1.4.1

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