caruby-core 1.4.7 → 1.4.9

Sign up to get free protection for your applications and to get access to all the features.
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)