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.
- 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)
|