caruby-core 1.4.7 → 1.4.9

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 (48) hide show
  1. data/History.txt +11 -0
  2. data/README.md +1 -1
  3. data/lib/caruby/cli/command.rb +27 -3
  4. data/lib/caruby/csv/csv_mapper.rb +2 -0
  5. data/lib/caruby/csv/csvio.rb +187 -169
  6. data/lib/caruby/database.rb +33 -16
  7. data/lib/caruby/database/lazy_loader.rb +23 -23
  8. data/lib/caruby/database/persistable.rb +32 -18
  9. data/lib/caruby/database/persistence_service.rb +20 -7
  10. data/lib/caruby/database/reader.rb +22 -21
  11. data/lib/caruby/database/search_template_builder.rb +7 -9
  12. data/lib/caruby/database/sql_executor.rb +52 -27
  13. data/lib/caruby/database/store_template_builder.rb +18 -13
  14. data/lib/caruby/database/writer.rb +107 -44
  15. data/lib/caruby/domain/attribute_metadata.rb +35 -25
  16. data/lib/caruby/domain/java_attribute_metadata.rb +43 -20
  17. data/lib/caruby/domain/merge.rb +9 -5
  18. data/lib/caruby/domain/reference_visitor.rb +4 -3
  19. data/lib/caruby/domain/resource_attributes.rb +52 -12
  20. data/lib/caruby/domain/resource_dependency.rb +129 -42
  21. data/lib/caruby/domain/resource_introspection.rb +1 -1
  22. data/lib/caruby/domain/resource_inverse.rb +20 -3
  23. data/lib/caruby/domain/resource_metadata.rb +20 -4
  24. data/lib/caruby/domain/resource_module.rb +190 -124
  25. data/lib/caruby/import/java.rb +39 -19
  26. data/lib/caruby/migration/migratable.rb +31 -6
  27. data/lib/caruby/migration/migrator.rb +126 -40
  28. data/lib/caruby/migration/uniquify.rb +0 -1
  29. data/lib/caruby/resource.rb +28 -5
  30. data/lib/caruby/util/attribute_path.rb +0 -2
  31. data/lib/caruby/util/class.rb +8 -5
  32. data/lib/caruby/util/collection.rb +5 -3
  33. data/lib/caruby/util/domain_extent.rb +0 -3
  34. data/lib/caruby/util/options.rb +10 -9
  35. data/lib/caruby/util/person.rb +41 -12
  36. data/lib/caruby/util/pretty_print.rb +1 -1
  37. data/lib/caruby/util/validation.rb +0 -28
  38. data/lib/caruby/version.rb +1 -1
  39. data/test/lib/caruby/import/java_test.rb +26 -9
  40. data/test/lib/caruby/migration/test_case.rb +103 -0
  41. data/test/lib/caruby/test_case.rb +231 -0
  42. data/test/lib/caruby/util/class_test.rb +2 -2
  43. data/test/lib/caruby/util/visitor_test.rb +3 -2
  44. data/test/lib/examples/galena/clinical_trials/migration/participant_test.rb +28 -0
  45. data/test/lib/examples/galena/clinical_trials/migration/test_case.rb +40 -0
  46. metadata +195 -170
  47. data/lib/caruby/domain/attribute_initializer.rb +0 -16
  48. data/test/lib/caruby/util/validation_test.rb +0 -14
@@ -8,17 +8,30 @@ require 'ftools'
8
8
  require 'date'
9
9
 
10
10
  require 'caruby/util/class'
11
+ require 'caruby/util/log'
11
12
  require 'caruby/util/inflector'
12
13
  require 'caruby/util/collection'
13
14
 
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
+
15
26
  # Adds the directories in the given path and all Java jar files contained in the directories
16
27
  # to the execution classpath.
17
28
  #
18
29
  # @param [String] path the colon or semi-colon separated directories
19
30
  def self.add_path(path)
31
+ # the path separator
32
+ sep = path[WINDOWS_PATH_SEP] ? WINDOWS_PATH_SEP : UNIX_PATH_SEP
20
33
  # the path directories
21
- dirs = path.split(/[:;]/).map { |dir| File.expand_path(dir) }
34
+ dirs = path.split(sep).map { |dir| File.expand_path(dir) }
22
35
  # Add all jars found anywhere within the directories to the the classpath.
23
36
  add_jars(*dirs)
24
37
  # Add the directories to the the classpath.
@@ -38,6 +51,10 @@ module Java
38
51
  #
39
52
  # @param [String] file the jar file or directory to add
40
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
41
58
  if file =~ /.jar$/ then
42
59
  # require is preferred to classpath append for a jar file
43
60
  require file
@@ -182,10 +199,6 @@ module Java
182
199
  time = Time.at(secs)
183
200
  # convert UTC timezone millisecond offset to Rational fraction of a day
184
201
  offset_millis = calendar.timeZone.getOffset(calendar.timeInMillis).to_f
185
- # adjust for DST
186
- if calendar.timeZone.useDaylightTime and time.isdst then
187
- offset_millis -= MILLIS_PER_HR
188
- end
189
202
  offset_days = offset_millis / MILLIS_PER_DAY
190
203
  offset_fraction = 1 / offset_days
191
204
  offset = Rational(1, offset_fraction)
@@ -206,15 +219,19 @@ module Java
206
219
  hour = min = sec = 0
207
220
  end
208
221
  # the Ruby time
209
- time = Time.mktime(date.year, date.mon, date.day, hour, min, sec)
222
+ rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, nil, nil)
210
223
  # millis since epoch
211
- millis = (time.to_f * 1000).truncate
224
+ millis = (rtime.to_f * 1000).truncate
212
225
  # the Java date factory
213
226
  calendar = java.util.Calendar.instance
214
- # adjust for DST
215
- if calendar.timeZone.useDaylightTime and Time.at(time).isdst then
216
- millis += MILLIS_PER_HR
217
- end
227
+ calendar.setTimeInMillis(millis)
228
+ jtime = calendar.getTime
229
+ # the daylight time flag
230
+ isdt = calendar.timeZone.inDaylightTime(jtime)
231
+ return jtime unless isdt
232
+ # adjust the Ruby time for DST
233
+ rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, isdt, nil)
234
+ millis = (rtime.to_f * 1000).truncate
218
235
  calendar.setTimeInMillis(millis)
219
236
  calendar.getTime
220
237
  end
@@ -224,15 +241,18 @@ module Java
224
241
  def self.now
225
242
  JavaUtil::Date.from_ruby_date(DateTime.now)
226
243
  end
227
-
228
- # Returns the Java package name for the full class_name, or nil if
229
- # class_name is unqualified.
230
- def self.java_package_name(class_name)
231
- prefix = class_name[/(\w+\.)+/]
232
- # remove the trailing period
233
- prefix.chop! if prefix
234
- prefix
244
+
245
+ # @param [Class, String] the JRuby class or the full Java class name
246
+ # @return (String, String] the package and base for the given name
247
+ def self.split_class_name(name_or_class)
248
+ name = Class === name_or_class ? name_or_class.java_class.name : name_or_class
249
+ match = NAME_SPLITTER_REGEX.match(name)
250
+ match ? match.captures : [nil, name]
235
251
  end
252
+
253
+ private
254
+
255
+ NAME_SPLITTER_REGEX = /^([\w.]+)\.(\w+)$/
236
256
  end
237
257
 
238
258
  class Class
@@ -118,28 +118,52 @@ module CaRuby
118
118
  # @param [{Symbol => String}] mth_hash a hash that associates this domain object's
119
119
  # attributes to migration method names
120
120
  def migrate_references(row, migrated, mth_hash=nil)
121
- migratable__set_references(self.class.saved_independent_attributes, row, migrated, mth_hash)
122
- migratable__set_references(self.class.unidirectional_dependent_attributes, row, migrated, mth_hash)
121
+ # migrate the owner
122
+ migratable__migrate_owner(row, migrated, mth_hash)
123
+ # migrate the remaining attributes
124
+ migratable__set_nonowner_references(self.class.saved_independent_attributes, row, migrated, mth_hash)
125
+ migratable__set_nonowner_references(self.class.unidirectional_dependent_attributes, row, migrated, mth_hash)
123
126
  end
124
127
 
125
128
  private
126
129
 
130
+ # Migrates the owner, if there is a unique owner in the migrated set.
131
+ #
132
+ # @param row (see #migrate_references)
133
+ # @param migrated (see #migrate_references)
134
+ # @param mth_hash (see #migrate_references)
135
+ def migratable__migrate_owner(row, migrated, mth_hash=nil)
136
+ # the owner attributes=> migrated reference hash
137
+ ovh = self.class.owner_attributes.to_compact_hash do |attr|
138
+ attr_md = self.class.attribute_metadata(attr)
139
+ migratable__target_value(attr_md, row, migrated, mth_hash=nil)
140
+ end
141
+ if ovh.size > 1 then
142
+ logger.debug { "The migrated dependent #{qp} has ambiguous migrated owner references #{ovh.qp}." }
143
+ elsif ovh.size == 1 then
144
+ attr, ref = ovh.first
145
+ set_attribute(attr, ref)
146
+ end
147
+ end
148
+
127
149
  # @param [AttributeMetadata::Filter] the attributes to set
128
150
  # @param row (see #migrate_references)
129
151
  # @param migrated (see #migrate_references)
130
152
  # @param mth_hash (see #migrate_references)
131
- def migratable__set_references(attr_filter, row, migrated, mth_hash=nil)
153
+ def migratable__set_nonowner_references(attr_filter, row, migrated, mth_hash=nil)
132
154
  attr_filter.each_pair do |attr, attr_md|
155
+ # skip owners
156
+ next if attr_md.owner?
133
157
  # the target value
134
158
  ref = migratable__target_value(attr_md, row, migrated, mth_hash) || next
135
159
  if attr_md.collection? then
136
160
  # the current value
137
161
  value = send(attr_md.reader) || next
138
162
  value << ref
139
- logger.debug { "Added migrated #{ref.qp} to #{qp} #{attribute}." }
163
+ logger.debug { "Added the migrated #{ref.qp} to #{qp} #{attr}." }
140
164
  else
141
165
  set_attribute(attr, ref)
142
- logger.debug { "Set #{qp} #{attr} to migrated #{ref.qp}." }
166
+ logger.debug { "Set the #{qp} #{attr} to migrated #{ref.qp}." }
143
167
  end
144
168
  end
145
169
  end
@@ -154,10 +178,11 @@ module CaRuby
154
178
  # the migrated references which are instances of the attribute type
155
179
  refs = migrated.select { |other| other != self and attr_md.type === other }
156
180
  # skip ambiguous references
181
+ if refs.size > 1 then logger.debug { "Migrator did not set references to ambiguous targets #{refs.pp_s}." } end
157
182
  return unless refs.size == 1
158
183
  # the single reference
159
184
  ref = refs.first
160
- # the shim method, if any
185
+ # the shim method, if any
161
186
  mth = mth_hash[attr_md.to_sym] if mth_hash
162
187
  # if there is a shim method, then call it
163
188
  mth && respond_to?(mth) ? send(mth, ref, row) : ref
@@ -16,7 +16,6 @@ require 'caruby/util/pretty_print'
16
16
  require 'caruby/util/properties'
17
17
  require 'caruby/util/collection'
18
18
  require 'caruby/migration/migratable'
19
- require 'caruby/migration/uniquify'
20
19
 
21
20
  module CaRuby
22
21
  class MigrationError < RuntimeError; end
@@ -35,11 +34,12 @@ module CaRuby
35
34
  # @option opts [String] :input required source file to migrate
36
35
  # @option opts [String] :shims optional array of shim files to load
37
36
  # @option opts [String] :unique ensures that migrated objects which include the {Resource::Unique}
38
- # mix-in do not conflict with existing or future objects (used for testing)
37
+ # @option opts [String] :create optional flag indicating that existing target objects are ignored
39
38
  # @option opts [String] :bad optional invalid record file
40
39
  # @option opts [Integer] :offset zero-based starting source record number to process (default 0)
41
40
  # @option opts [Boolean] :quiet suppress output messages
42
41
  def initialize(opts)
42
+ @rec_cnt = 0
43
43
  parse_options(opts)
44
44
  build
45
45
  end
@@ -49,6 +49,9 @@ module CaRuby
49
49
  # If a block is given to this method, then the block is called on each stored
50
50
  # migration target object.
51
51
  #
52
+ # If the +:create+ option is set, then an input record for a target object which already
53
+ # exists in the database is noted in a debug log message and ignored rather than updated.
54
+ #
52
55
  # @yield [target] operation performed on the migration target
53
56
  # @yieldparam [Resource] target the migrated target domain object
54
57
  def migrate_to_database(&block)
@@ -75,6 +78,9 @@ module CaRuby
75
78
 
76
79
  # Class {#migrate} with a {#save} block.
77
80
  def execute_save
81
+ if @database.nil? then
82
+ raise MigrationError.new("Migrator cannot save records since the database option was not specified.")
83
+ end
78
84
  @database.open do |db|
79
85
  migrate do |target|
80
86
  save(target, db)
@@ -108,29 +114,35 @@ module CaRuby
108
114
  @input = Options.get(:input, opts)
109
115
  raise MigrationError.new("Migrator missing required source file parameter") if @input.nil?
110
116
  @database = opts[:database]
111
- raise MigrationError.new("Migrator missing required database parameter") if @database.nil?
112
117
  @target_class = opts[:target]
113
118
  raise MigrationError.new("Migrator missing required target class parameter") if @target_class.nil?
114
119
  @bad_rec_file = opts[:bad]
115
- logger.info("Migration options: #{opts.reject { |option, value| value.nil_or_empty? }.pp_s}.")
120
+ @create = opts[:create]
121
+ logger.info("Migration options: #{printable_options(opts).pp_s}.")
116
122
  # flag indicating whether to print a progress monitor
117
123
  @print_progress = !opts[:quiet]
118
124
  end
125
+
126
+ def printable_options(opts)
127
+ popts = opts.reject { |option, value| value.nil_or_empty? }
128
+ # The target class should be a simple class name rather than the class metadata.
129
+ popts[:target] = popts[:target].qp if popts.has_key?(:target)
130
+ popts
131
+ end
119
132
 
120
133
  def build
121
134
  # the current source class => instance map
122
135
  raise MigrationError.new("No file to migrate") if @input.nil?
123
136
 
124
137
  # make a CSV loader which only converts input fields corresponding to non-String attributes
125
- logger.info { "Migration input file: #{@input}." }
126
- @loader = CsvIO.new(@input) do |value, info|
127
- value unless @nonstring_headers.include?(info.header)
128
- end
138
+ logger.info("Migration input file: #{@input}.")
139
+ @loader = CsvIO.new(@input, &method(:convert))
140
+ logger.debug { "Migration data input file #{@input} headers: #{@loader.headers.qp}" }
129
141
 
130
- # create the class => path => header hash
131
- fld_map = load_field_map(@fld_map_file)
132
142
  # create the class => path => default value hash
133
143
  @def_hash = @def_file ? load_defaults(@def_file) : LazyHash.new { Hash.new }
144
+ # create the class => path => header hash
145
+ fld_map = load_field_map(@fld_map_file)
134
146
  # create the class => paths hash
135
147
  @cls_paths_hash = create_class_paths_hash(fld_map, @def_hash)
136
148
  # create the path => class => header hash
@@ -139,6 +151,12 @@ module CaRuby
139
151
  @cls_paths_hash.keys.each { |klass| add_owners(klass) }
140
152
  # order the creatable classes by dependency, owners first, to smooth the migration process
141
153
  @creatable_classes = @cls_paths_hash.keys.sort! { |klass, other| other.depends_on?(klass) ? -1 : (klass.depends_on?(other) ? 1 : 0) }
154
+ @creatable_classes.each do |klass|
155
+ if klass.abstract? then
156
+ raise MigrationError.new("Migrator cannot create the abstract class #{klass}; specify a subclass instead in the mapping file.")
157
+ end
158
+ end
159
+
142
160
  # print the maps
143
161
  print_hash = LazyHash.new { Hash.new }
144
162
  @cls_paths_hash.each do |klass, paths|
@@ -166,7 +184,27 @@ module CaRuby
166
184
  @nonstring_headers.merge!(cls_hdr_hash.values) if attr_md.type != Java::JavaLang::String
167
185
  end
168
186
  end
169
-
187
+
188
+ # Converts the given input field value as follows:
189
+ # * if the info header is a String field, then return the value unchanged
190
+ # * otherwise, if the value is a case-insensitive match for +true+ or +false+, then convert
191
+ # the value to the respective Boolean
192
+ # * otherwise, return nil which will delegate to the generic CsvIO converter
193
+ # @param (see CsvIO#convert)
194
+ # @yield (see CsvIO#convert)
195
+ def convert(value, info)
196
+ @nonstring_headers.include?(info.header) ? convert_boolean(value) : value
197
+ end
198
+
199
+ # @param [String] value the input value
200
+ # @return [Boolean, nil] the corresponding boolean, or nil if none
201
+ def convert_boolean(value)
202
+ case value
203
+ when /true/i then true
204
+ when /false/i then false
205
+ end
206
+ end
207
+
170
208
  # Adds missing klass owner classes to the migration class path hash (with empty paths).
171
209
  def add_owners(klass)
172
210
  klass.owners.each do |owner|
@@ -288,13 +326,13 @@ module CaRuby
288
326
  @loader.trash = @bad_rec_file
289
327
  logger.info("Unmigrated records will be written to #{File.expand_path(@bad_rec_file)}.")
290
328
  end
291
- rec_cnt = mgt_cnt = 0
329
+ @rec_cnt = mgt_cnt = 0
292
330
  logger.info { "Migrating #{@input}..." }
293
331
  @loader.each do |row|
294
332
  # the one-based current record number
295
- rec_no = rec_cnt + 1
333
+ rec_no = @rec_cnt + 1
296
334
  # skip if the row precedes the offset option
297
- rec_cnt += 1 && next if rec_cnt < @offset
335
+ @rec_cnt += 1 && next if @rec_cnt < @offset
298
336
  begin
299
337
  # migrate the row
300
338
  logger.debug { "Migrating record #{rec_no}..." }
@@ -328,9 +366,9 @@ module CaRuby
328
366
  raise MigrationError.new("Migration not performed on record #{rec_no}")
329
367
  end
330
368
  end
331
- rec_cnt += 1
369
+ @rec_cnt += 1
332
370
  end
333
- logger.info("Migrated #{mgt_cnt} of #{rec_cnt} records.")
371
+ logger.info("Migrated #{mgt_cnt} of #{@rec_cnt} records.")
334
372
  end
335
373
 
336
374
  # Prints a +\++ progress indicator to stdout.
@@ -359,15 +397,29 @@ module CaRuby
359
397
  # migrate each object from the input row
360
398
  created.each { |obj| obj.migrate(row, migrated) }
361
399
  # remove invalid migrations
362
- migrated.delete_if { |obj| not migration_valid?(obj) }
400
+ valid, invalid = migrated.partition { |obj| migration_valid?(obj) }
363
401
  # set the references
364
- migrated.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
402
+ valid.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
365
403
  # the target object
366
- target = migrated.detect { |obj| @target_class === obj }
367
- if target then
368
- logger.debug { "Migrated target #{target}." }
369
- target
370
- end
404
+ target = valid.detect { |obj| @target_class === obj } || return
405
+ # the target is invalid if it has an invalid owner
406
+ return unless owner_valid?(target, valid, invalid)
407
+ logger.debug { "Migrated target #{target}." }
408
+ target
409
+ end
410
+
411
+ # Returns whether the given domain object satisfies at least one of the following conditions:
412
+ # * it has an owner among the valid objects
413
+ # * it does not have an owner among the invalid objects
414
+ #
415
+ # @param [Resource] obj the domain object to check
416
+ # @param [<Resource>] valid the valid migrated objects
417
+ # @param [<Resource>] invalid the invalid migrated objects
418
+ # @return [Boolean] whether the domain object is valid
419
+ def owner_valid?(obj, valid, invalid)
420
+ otypes = obj.class.owners
421
+ invalid.all? { |other| not otypes.include?(other.class) } or
422
+ valid.any? { |other| otypes.include?(other.class) }
371
423
  end
372
424
 
373
425
  # @param [Migratable] obj the migrated object
@@ -391,10 +443,11 @@ module CaRuby
391
443
  # @return [Resource] the new instance
392
444
  def create(klass, row, created)
393
445
  # the new object
446
+ logger.debug { "Migrator building #{klass.qp}..." }
394
447
  created << obj = klass.new
395
448
  migrate_attributes(obj, row, created)
396
449
  add_defaults(obj, row, created)
397
- logger.debug { "Migrator created #{obj}." }
450
+ logger.debug { "Migrator built #{obj}." }
398
451
  obj
399
452
  end
400
453
 
@@ -441,22 +494,22 @@ module CaRuby
441
494
  end
442
495
  end
443
496
 
444
- # Sets the given parent reference AttributeMetadata attr_md attribute to a new domain object.
497
+ # Sets the given migrated object's reference attribute to a new referenced domain object.
445
498
  #
446
- # @param [Resource] parent the domain object being migrated
499
+ # @param [Resource] obj the domain object being migrated
447
500
  # @param [AttributeMetadata] attr_md the attribute being migrated
448
501
  # @param row (see #create)
449
502
  # @param created (see #create)
450
503
  # @return the new object
451
- def create_reference(parent, attr_md, row, created)
504
+ def create_reference(obj, attr_md, row, created)
452
505
  if attr_md.type.abstract? then
453
- raise MigrationError.new("Cannot create #{parent.qp} #{attr_md} with abstract type #{attr_md.type}")
506
+ raise MigrationError.new("Cannot create #{obj.qp} #{attr_md} with abstract type #{attr_md.type}")
454
507
  end
455
508
  ref = attr_md.type.new
456
509
  ref.migrate(row, Array::EMPTY_ARRAY)
457
- parent.send(attr_md.writer, ref)
510
+ obj.send(attr_md.writer, ref)
458
511
  created << ref
459
- logger.debug { "Migrator created #{parent.qp} #{attr_md} #{ref}." }
512
+ logger.debug { "Migrator created #{obj.qp} #{attr_md} #{ref}." }
460
513
  ref
461
514
  end
462
515
 
@@ -472,8 +525,8 @@ module CaRuby
472
525
  # set the attribute
473
526
  begin
474
527
  obj.send(attr_md.writer, value)
475
- rescue Exception => e
476
- raise MigrationError.new("Could not set #{obj.qp} #{attr_md} to #{value.qp} - #{e}")
528
+ rescue Exception
529
+ raise MigrationError.new("Could not set #{obj.qp} #{attr_md} to #{value.qp} - #{$!}")
477
530
  end
478
531
  logger.debug { "Migrated #{obj.qp} #{attr_md} to #{value}." }
479
532
  end
@@ -481,9 +534,23 @@ module CaRuby
481
534
  # @param [Resource] obj the domain object to save in the database
482
535
  # @return [Resource, nil] obj if the save is successful, nil otherwise
483
536
  def save(obj, database)
484
- logger.debug { "Migrator saving #{obj}..." }
485
- database.save(obj)
486
- logger.debug { "Migrator saved #{obj}." }
537
+ if @create then
538
+ if database.find(obj) then
539
+ logger.debug { "Migrator ignored record #{current_record}, since it already exists as #{obj.printable_content(obj.class.secondary_key_attributes)} with id #{obj.identifier}." }
540
+ else
541
+ logger.debug { "Migrator creating #{obj}..." }
542
+ database.create(obj)
543
+ logger.debug { "Migrator creating #{obj}." }
544
+ end
545
+ else
546
+ logger.debug { "Migrator saving #{obj}..." }
547
+ database.save(obj)
548
+ logger.debug { "Migrator saved #{obj}." }
549
+ end
550
+ end
551
+
552
+ def current_record
553
+ @rec_cnt + 1
487
554
  end
488
555
 
489
556
  # @param [String] file the migration fields configuration file
@@ -504,6 +571,7 @@ module CaRuby
504
571
  # the header accessor method for the field
505
572
  header = @loader.accessor(field)
506
573
  raise MigrationError.new("Field defined in migration configuration #{file} not found in input file #{@input} headers: #{field}") if header.nil?
574
+ # associate each attribute path in the property value with the header
507
575
  attr_list.split(/,\s*/).each do |path_s|
508
576
  klass, path = create_attribute_path(path_s)
509
577
  map[klass][path] = header
@@ -512,6 +580,8 @@ module CaRuby
512
580
 
513
581
  # include the target class
514
582
  map[@target_class] ||= Hash.new
583
+ # include the default classes
584
+ @def_hash.each_key { |klass| map[klass] ||= Hash.new }
515
585
 
516
586
  # add superclass paths into subclass paths
517
587
  map.each do |klass, path_hdr_hash|
@@ -524,7 +594,7 @@ module CaRuby
524
594
  end
525
595
 
526
596
  # include only concrete classes
527
- classes = map.enum_keys
597
+ classes = map.keys
528
598
  map.delete_if do |klass, paths|
529
599
  klass.abstract? or classes.any? { |other| other < klass }
530
600
  end
@@ -565,8 +635,15 @@ module CaRuby
565
635
  # build the AttributeMetadata path
566
636
  path = []
567
637
  names.inject(klass) do |parent, name|
568
- attr_md = parent.attribute_metadata(name.to_sym) rescue nil
569
- raise MigrationError.new("Migration field mapping attribute not found: #{parent.qp}.#{name}") if attr_md.nil?
638
+ attr = name.to_sym
639
+ attr_md = begin
640
+ parent.attribute_metadata(attr)
641
+ rescue NameError
642
+ raise MigrationError.new("Migration field mapping attribute #{parent.qp}.#{attr} not found: #{$!}")
643
+ end
644
+ if attr_md.collection? then
645
+ raise MigrationError.new("Migration field mapping attribute #{parent.qp}.#{attr} is a collection, which is not supported")
646
+ end
570
647
  path << attr_md
571
648
  attr_md.type
572
649
  end
@@ -577,12 +654,21 @@ module CaRuby
577
654
  [klass, path]
578
655
  end
579
656
 
657
+ # @param [String] name the class name, without the {#context_module}
658
+ # @return [Class] the corresponding class
580
659
  def class_for_name(name)
581
660
  # navigate through the scope to the final class
582
- name.split('::').inject(@target_class.domain_module) do |scope, cnm|
583
- scope.const_get(cnm)
661
+ name.split('::').inject(context_module) do |ctxt, cnm|
662
+ ctxt.const_get(cnm)
584
663
  end
585
664
  end
665
+
666
+ # The context module is given by the target class {ResourceClass#domain_module}.
667
+ #
668
+ # @return [Module] the class name resolution context
669
+ def context_module
670
+ @target_class.domain_module
671
+ end
586
672
 
587
673
  # @return a new class => [paths] hash from the migration fields configuration map
588
674
  def create_class_paths_hash(fld_map, def_map)