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.
- data/History.txt +11 -0
- data/README.md +1 -1
- data/lib/caruby/cli/command.rb +27 -3
- data/lib/caruby/csv/csv_mapper.rb +2 -0
- data/lib/caruby/csv/csvio.rb +187 -169
- data/lib/caruby/database.rb +33 -16
- data/lib/caruby/database/lazy_loader.rb +23 -23
- data/lib/caruby/database/persistable.rb +32 -18
- data/lib/caruby/database/persistence_service.rb +20 -7
- data/lib/caruby/database/reader.rb +22 -21
- data/lib/caruby/database/search_template_builder.rb +7 -9
- data/lib/caruby/database/sql_executor.rb +52 -27
- data/lib/caruby/database/store_template_builder.rb +18 -13
- data/lib/caruby/database/writer.rb +107 -44
- data/lib/caruby/domain/attribute_metadata.rb +35 -25
- data/lib/caruby/domain/java_attribute_metadata.rb +43 -20
- data/lib/caruby/domain/merge.rb +9 -5
- data/lib/caruby/domain/reference_visitor.rb +4 -3
- data/lib/caruby/domain/resource_attributes.rb +52 -12
- data/lib/caruby/domain/resource_dependency.rb +129 -42
- data/lib/caruby/domain/resource_introspection.rb +1 -1
- data/lib/caruby/domain/resource_inverse.rb +20 -3
- data/lib/caruby/domain/resource_metadata.rb +20 -4
- data/lib/caruby/domain/resource_module.rb +190 -124
- data/lib/caruby/import/java.rb +39 -19
- data/lib/caruby/migration/migratable.rb +31 -6
- data/lib/caruby/migration/migrator.rb +126 -40
- data/lib/caruby/migration/uniquify.rb +0 -1
- data/lib/caruby/resource.rb +28 -5
- data/lib/caruby/util/attribute_path.rb +0 -2
- data/lib/caruby/util/class.rb +8 -5
- data/lib/caruby/util/collection.rb +5 -3
- data/lib/caruby/util/domain_extent.rb +0 -3
- data/lib/caruby/util/options.rb +10 -9
- data/lib/caruby/util/person.rb +41 -12
- data/lib/caruby/util/pretty_print.rb +1 -1
- data/lib/caruby/util/validation.rb +0 -28
- data/lib/caruby/version.rb +1 -1
- data/test/lib/caruby/import/java_test.rb +26 -9
- data/test/lib/caruby/migration/test_case.rb +103 -0
- data/test/lib/caruby/test_case.rb +231 -0
- data/test/lib/caruby/util/class_test.rb +2 -2
- data/test/lib/caruby/util/visitor_test.rb +3 -2
- data/test/lib/examples/galena/clinical_trials/migration/participant_test.rb +28 -0
- data/test/lib/examples/galena/clinical_trials/migration/test_case.rb +40 -0
- metadata +195 -170
- data/lib/caruby/domain/attribute_initializer.rb +0 -16
- data/test/lib/caruby/util/validation_test.rb +0 -14
data/lib/caruby/import/java.rb
CHANGED
@@ -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(
|
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
|
-
|
222
|
+
rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, nil, nil)
|
210
223
|
# millis since epoch
|
211
|
-
millis = (
|
224
|
+
millis = (rtime.to_f * 1000).truncate
|
212
225
|
# the Java date factory
|
213
226
|
calendar = java.util.Calendar.instance
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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
|
-
#
|
229
|
-
#
|
230
|
-
def self.
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
122
|
-
|
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
|
-
|
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} #{
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
126
|
-
@loader = CsvIO.new(@input
|
127
|
-
|
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.
|
400
|
+
valid, invalid = migrated.partition { |obj| migration_valid?(obj) }
|
363
401
|
# set the references
|
364
|
-
|
402
|
+
valid.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
|
365
403
|
# the target object
|
366
|
-
target =
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
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
|
497
|
+
# Sets the given migrated object's reference attribute to a new referenced domain object.
|
445
498
|
#
|
446
|
-
# @param [Resource]
|
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(
|
504
|
+
def create_reference(obj, attr_md, row, created)
|
452
505
|
if attr_md.type.abstract? then
|
453
|
-
raise MigrationError.new("Cannot create #{
|
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
|
-
|
510
|
+
obj.send(attr_md.writer, ref)
|
458
511
|
created << ref
|
459
|
-
logger.debug { "Migrator created #{
|
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
|
476
|
-
raise MigrationError.new("Could not set #{obj.qp} #{attr_md} to #{value.qp} - #{
|
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
|
-
|
485
|
-
|
486
|
-
|
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.
|
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
|
-
|
569
|
-
|
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(
|
583
|
-
|
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)
|