caruby-core 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/doc/website/css/site.css +1 -5
- data/doc/website/images/avatar.png +0 -0
- data/doc/website/images/favicon.ico +0 -0
- data/doc/website/images/logo.png +0 -0
- data/doc/website/index.html +82 -0
- data/doc/website/install.html +87 -0
- data/doc/website/quick_start.html +87 -0
- data/doc/website/tissue.html +85 -0
- data/doc/website/uom.html +10 -0
- data/lib/caruby.rb +3 -0
- data/lib/caruby/active_support/README.txt +2 -0
- data/lib/caruby/active_support/core_ext/string.rb +7 -0
- data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/caruby/active_support/inflections.rb +55 -0
- data/lib/caruby/active_support/inflector.rb +398 -0
- data/lib/caruby/cli/application.rb +36 -0
- data/lib/caruby/cli/command.rb +169 -0
- data/lib/caruby/csv/csv_mapper.rb +157 -0
- data/lib/caruby/csv/csvio.rb +185 -0
- data/lib/caruby/database.rb +252 -0
- data/lib/caruby/database/fetched_matcher.rb +66 -0
- data/lib/caruby/database/persistable.rb +432 -0
- data/lib/caruby/database/persistence_service.rb +162 -0
- data/lib/caruby/database/reader.rb +599 -0
- data/lib/caruby/database/saved_merger.rb +131 -0
- data/lib/caruby/database/search_template_builder.rb +59 -0
- data/lib/caruby/database/sql_executor.rb +75 -0
- data/lib/caruby/database/store_template_builder.rb +200 -0
- data/lib/caruby/database/writer.rb +469 -0
- data/lib/caruby/domain/annotatable.rb +25 -0
- data/lib/caruby/domain/annotation.rb +23 -0
- data/lib/caruby/domain/attribute_metadata.rb +447 -0
- data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
- data/lib/caruby/domain/merge.rb +91 -0
- data/lib/caruby/domain/properties.rb +95 -0
- data/lib/caruby/domain/reference_visitor.rb +289 -0
- data/lib/caruby/domain/resource_attributes.rb +528 -0
- data/lib/caruby/domain/resource_dependency.rb +205 -0
- data/lib/caruby/domain/resource_introspection.rb +159 -0
- data/lib/caruby/domain/resource_metadata.rb +117 -0
- data/lib/caruby/domain/resource_module.rb +285 -0
- data/lib/caruby/domain/uniquify.rb +38 -0
- data/lib/caruby/import/annotatable_class.rb +28 -0
- data/lib/caruby/import/annotation_class.rb +27 -0
- data/lib/caruby/import/annotation_module.rb +67 -0
- data/lib/caruby/import/java.rb +338 -0
- data/lib/caruby/migration/migratable.rb +167 -0
- data/lib/caruby/migration/migrator.rb +533 -0
- data/lib/caruby/migration/resource.rb +8 -0
- data/lib/caruby/migration/resource_module.rb +11 -0
- data/lib/caruby/migration/uniquify.rb +20 -0
- data/lib/caruby/resource.rb +969 -0
- data/lib/caruby/util/attribute_path.rb +46 -0
- data/lib/caruby/util/cache.rb +53 -0
- data/lib/caruby/util/class.rb +99 -0
- data/lib/caruby/util/collection.rb +1053 -0
- data/lib/caruby/util/controlled_value.rb +35 -0
- data/lib/caruby/util/coordinate.rb +75 -0
- data/lib/caruby/util/domain_extent.rb +49 -0
- data/lib/caruby/util/file_separator.rb +65 -0
- data/lib/caruby/util/inflector.rb +20 -0
- data/lib/caruby/util/log.rb +95 -0
- data/lib/caruby/util/math.rb +12 -0
- data/lib/caruby/util/merge.rb +59 -0
- data/lib/caruby/util/module.rb +34 -0
- data/lib/caruby/util/options.rb +92 -0
- data/lib/caruby/util/partial_order.rb +36 -0
- data/lib/caruby/util/person.rb +119 -0
- data/lib/caruby/util/pretty_print.rb +184 -0
- data/lib/caruby/util/properties.rb +112 -0
- data/lib/caruby/util/stopwatch.rb +66 -0
- data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
- data/lib/caruby/util/transitive_closure.rb +45 -0
- data/lib/caruby/util/tree.rb +48 -0
- data/lib/caruby/util/trie.rb +37 -0
- data/lib/caruby/util/uniquifier.rb +30 -0
- data/lib/caruby/util/validation.rb +48 -0
- data/lib/caruby/util/version.rb +56 -0
- data/lib/caruby/util/visitor.rb +351 -0
- data/lib/caruby/util/weak_hash.rb +36 -0
- data/lib/caruby/version.rb +3 -0
- metadata +186 -0
@@ -0,0 +1,533 @@
|
|
1
|
+
# load the required gems
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
# the UOM gem
|
5
|
+
gem 'uom'
|
6
|
+
|
7
|
+
require 'enumerator'
|
8
|
+
require 'date'
|
9
|
+
require 'uom'
|
10
|
+
require 'caruby/csv/csvio'
|
11
|
+
require 'caruby/util/class'
|
12
|
+
require 'caruby/util/log'
|
13
|
+
require 'caruby/util/inflector'
|
14
|
+
require 'caruby/util/options'
|
15
|
+
require 'caruby/util/pretty_print'
|
16
|
+
require 'caruby/util/properties'
|
17
|
+
require 'caruby/util/collection'
|
18
|
+
require 'caruby/migration/resource'
|
19
|
+
|
20
|
+
module CaRuby
|
21
|
+
class MigrationError < RuntimeError; end
|
22
|
+
|
23
|
+
# Migrates a CSV extract to a caBIG application.
|
24
|
+
class Migrator
|
25
|
+
include Enumerable
|
26
|
+
|
27
|
+
# Creates a new Migrator.
|
28
|
+
#
|
29
|
+
# @param [{Symbol => Object}] opts the migration options
|
30
|
+
# @option opts [String] :database target application {CaRuby::Database}
|
31
|
+
# @option opts [String] :target required target domain class
|
32
|
+
# @option opts [String] :input required source file to migrate
|
33
|
+
# @option opts [String] :shims optional array of shim files to load
|
34
|
+
# @option opts [String] :bad write each invalid record to the given file and continue migration
|
35
|
+
# @option opts [String] :offset zero-based starting source record number to process (default 0)
|
36
|
+
def initialize(opts)
|
37
|
+
parse_options(opts)
|
38
|
+
build
|
39
|
+
end
|
40
|
+
|
41
|
+
# Imports this migrator's file into the database with the given connect options.
|
42
|
+
# This method creates or updates the domain objects mapped from the import source.
|
43
|
+
# If a block is given to this method, then the block is called on each stored
|
44
|
+
# migration target object.
|
45
|
+
#
|
46
|
+
# @yield [target] operation performed on the migration target
|
47
|
+
# @yieldparam [Resource] target the migrated target domain object
|
48
|
+
def migrate_to_database(&block)
|
49
|
+
# migrate with save
|
50
|
+
tm = Stopwatch.measure { execute_save(&block) }.elapsed
|
51
|
+
logger.debug { format_migration_time_log_message(tm) }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Imports this migrator's CSV file and calls the required block on each migrated target
|
55
|
+
# domain object.
|
56
|
+
#
|
57
|
+
# @yield [target] operation performed on the migration target
|
58
|
+
# @yieldparam [Resource] target the migrated target domain object
|
59
|
+
def migrate(&block)
|
60
|
+
raise MigrationError.new("No migration block") unless block_given?
|
61
|
+
migrate_rows(&block)
|
62
|
+
end
|
63
|
+
|
64
|
+
alias :each :migrate
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Class {#migrate} with a {#save} block.
|
69
|
+
def execute_save
|
70
|
+
@database.open do |db|
|
71
|
+
migrate do |target|
|
72
|
+
save(target, db)
|
73
|
+
yield target if block_given?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return a log message String for the given migration time in seconds
|
79
|
+
def format_migration_time_log_message(time)
|
80
|
+
# the database execution time
|
81
|
+
dt = @database.execution_time
|
82
|
+
if time > 120 then
|
83
|
+
time /= 60
|
84
|
+
dt /= 60
|
85
|
+
unit = "minutes"
|
86
|
+
else
|
87
|
+
unit = "seconds"
|
88
|
+
end
|
89
|
+
"Migration took #{'%.2f' % time} #{unit}, of which #{'%.2f' % dt} were database operations."
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_options(opts)
|
93
|
+
@fld_map_file = opts[:mapping]
|
94
|
+
raise MigrationError.new("Migrator missing required field mapping file parameter") if @fld_map_file.nil?
|
95
|
+
@shims = opts[:shims] ||= []
|
96
|
+
@offset = opts[:offset] ||= 0
|
97
|
+
@input = Options.get(:input, opts)
|
98
|
+
raise MigrationError.new("Migrator missing required source file parameter") if @input.nil?
|
99
|
+
@database = Options.get(:database, opts)
|
100
|
+
raise MigrationError.new("Migrator missing required database parameter") if @database.nil?
|
101
|
+
@target_class = Options.get(:target, opts)
|
102
|
+
raise MigrationError.new("Migrator missing required target class parameter") if @target_class.nil?
|
103
|
+
@bad_rec_file = opts[:bad]
|
104
|
+
logger.info("Migration options: #{opts.reject { |option, value| value.nil_or_empty? }.pp_s}.")
|
105
|
+
end
|
106
|
+
|
107
|
+
def build
|
108
|
+
# the current source class => instance map
|
109
|
+
raise MigrationError.new("No file to migrate") if @input.nil?
|
110
|
+
|
111
|
+
# make a CSV loader which only converts input fields corresponding to non-String attributes
|
112
|
+
logger.info { "Migration input file: #{@input}." }
|
113
|
+
@loader = CsvIO.new(@input) do |value, info|
|
114
|
+
value unless @nonstring_headers.include?(info.header)
|
115
|
+
end
|
116
|
+
|
117
|
+
# create the class => path => header hash
|
118
|
+
fld_map = load_field_map(@fld_map_file)
|
119
|
+
# create the class => paths hash
|
120
|
+
@cls_paths_hash = create_class_paths_hash(fld_map)
|
121
|
+
# create the path => class => header hash
|
122
|
+
@header_map = create_header_map(fld_map)
|
123
|
+
# add missing owner classes (copy the keys rather than using each_key since the hash is updated)
|
124
|
+
@cls_paths_hash.keys.each { |klass| add_owners(klass) }
|
125
|
+
# order the creatable classes by dependency, owners first, to smooth the migration process
|
126
|
+
@creatable_classes = @cls_paths_hash.keys.sort! { |klass, other| other.depends_on?(klass) ? -1 : (klass.depends_on?(other) ? 1 : 0) }
|
127
|
+
# print the maps
|
128
|
+
print_hash = LazyHash.new { Hash.new }
|
129
|
+
@cls_paths_hash.each do |klass, paths|
|
130
|
+
print_hash[klass.qp] = paths.map { |path| {path.map { |attr_md| attr_md.to_sym }.join('.') => @header_map[path][klass] } }
|
131
|
+
end
|
132
|
+
logger.info { "Migration paths:\n#{print_hash.pp_s}" }
|
133
|
+
logger.info { "Migration creatable classes: #{@creatable_classes.qp}." }
|
134
|
+
|
135
|
+
# add shim modifiers
|
136
|
+
load_shims(@shims)
|
137
|
+
|
138
|
+
# the class => attribute migration methods hash
|
139
|
+
create_migration_method_hashes
|
140
|
+
|
141
|
+
# collect the String input fields for the custom CSVLoader converter
|
142
|
+
@nonstring_headers = Set.new
|
143
|
+
logger.info("Migration attributes:")
|
144
|
+
@header_map.each do |path, cls_hdr_hash|
|
145
|
+
attr_md = path.last
|
146
|
+
cls_hdr_hash.each do |klass, hdr|
|
147
|
+
type_s = attr_md.type ? attr_md.type.qp : 'Object'
|
148
|
+
logger.info(" #{hdr} => #{klass.qp}.#{path.join('.')} (#{type_s})")
|
149
|
+
end
|
150
|
+
@nonstring_headers.merge!(cls_hdr_hash.values) if attr_md.type != Java::JavaLang::String
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Adds missing klass owner classes to the migration class path hash (with empty paths).
|
155
|
+
def add_owners(klass)
|
156
|
+
klass.owners.each do |owner|
|
157
|
+
next if @cls_paths_hash.detect_key { |other| other <= owner } or owner.abstract?
|
158
|
+
logger.debug { "Migrator adding #{klass.qp} owner #{owner.qp}" }
|
159
|
+
@cls_paths_hash[owner] = Array::EMPTY_ARRAY
|
160
|
+
add_owners(owner)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Creates the class => +migrate_+_<attribute>_ hash for the given klasses.
|
165
|
+
def create_migration_method_hashes
|
166
|
+
# the attribute metadata => migration method hash variable
|
167
|
+
@attr_md_mgt_mth_map = {}
|
168
|
+
# the class => attribute => migration method hash variable
|
169
|
+
@mgt_mth_hash = {}
|
170
|
+
# collect the migration methods
|
171
|
+
customizable_class_attributes.each { |klass, attr_mds| add_migration_methods(klass, attr_mds) }
|
172
|
+
# print the migration shim methods
|
173
|
+
unless @mgt_mth_hash.empty? then
|
174
|
+
printer_hash = LazyHash.new { Array.new }
|
175
|
+
@mgt_mth_hash.each do |klass, attr_mth_hash|
|
176
|
+
mthds = attr_mth_hash.values
|
177
|
+
printer_hash[klass.qp] = mthds unless mthds.empty?
|
178
|
+
end
|
179
|
+
logger.info("Migration shim methods: #{printer_hash.pp_s}.")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# @return the class => attributes hash for terminal path attributes which can be customized by +migrate_+ methods
|
184
|
+
def customizable_class_attributes
|
185
|
+
# The customizable classes set, starting with creatable classes and adding in
|
186
|
+
# the migration path terminal attribute declarer classes below.
|
187
|
+
klasses = @creatable_classes.to_set
|
188
|
+
# the class => path terminal attributes hash
|
189
|
+
cls_attrs_hash = LazyHash.new { Set.new }
|
190
|
+
# add each path terminal attribute and its declarer class
|
191
|
+
@cls_paths_hash.each_value do |paths|
|
192
|
+
paths.each do |path|
|
193
|
+
attr_md = path.last
|
194
|
+
type = attr_md.declarer
|
195
|
+
klasses << type
|
196
|
+
cls_attrs_hash[type] << attr_md
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Merge each redundant customizable superclass into its concrete customizable subclasses.
|
201
|
+
klasses.dup.each do |cls|
|
202
|
+
redundant = false
|
203
|
+
klasses.each do |other|
|
204
|
+
# cls is redundant if it is a superclass of other
|
205
|
+
redundant = other < cls
|
206
|
+
if redundant then
|
207
|
+
cls_attrs_hash[other].merge!(cls_attrs_hash[cls])
|
208
|
+
end
|
209
|
+
end
|
210
|
+
# remove the redundant class
|
211
|
+
if redundant then
|
212
|
+
cls_attrs_hash.delete(cls)
|
213
|
+
klasses.delete(cls)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
cls_attrs_hash
|
218
|
+
end
|
219
|
+
|
220
|
+
# Discovers methods of the form +migrate+__attribute_ implemented for the paths
|
221
|
+
# in the given class => paths hash the given klass. The migrate method is called
|
222
|
+
# on the input field value corresponding to the path.
|
223
|
+
def add_migration_methods(klass, attr_mds)
|
224
|
+
# the migrate methods, excluding the Migratable migrate_references method
|
225
|
+
mths = klass.instance_methods(true).select { |mth| mth =~ /^migrate.(?!references)/ }
|
226
|
+
return if mths.empty?
|
227
|
+
|
228
|
+
# the attribute => migration method hash
|
229
|
+
attr_mth_hash = {}
|
230
|
+
mths.each do |mth|
|
231
|
+
# the attribute suffix, e.g. name for migrate_name or Name for migrateName
|
232
|
+
suffix = /^migrate(_)?(.*)/.match(mth).captures[1]
|
233
|
+
# the attribute name
|
234
|
+
attr_nm = suffix[0, 1].downcase + suffix[1..-1]
|
235
|
+
# the attribute for the name, or skip if no such attribute
|
236
|
+
attr = klass.standard_attribute(attr_nm) rescue next
|
237
|
+
# associate the attribute => method
|
238
|
+
attr_mth_hash[attr] = mth
|
239
|
+
end
|
240
|
+
|
241
|
+
# for each class path terminal attribute metadata, add the migration methods
|
242
|
+
# to the attribute metadata => migration method hash
|
243
|
+
attr_mds.each do |attr_md|
|
244
|
+
# the attribute migration method
|
245
|
+
mth = attr_mth_hash[attr_md.to_sym]
|
246
|
+
# associate the AttributeMetadata => method
|
247
|
+
@attr_md_mgt_mth_map[attr_md] ||= mth if mth
|
248
|
+
end
|
249
|
+
@mgt_mth_hash[klass] = attr_mth_hash
|
250
|
+
end
|
251
|
+
|
252
|
+
# loads the shim files.
|
253
|
+
def load_shims(files)
|
254
|
+
logger.debug { "Loading shims with load path #{$:.pp_s}..." }
|
255
|
+
files.each do |file|
|
256
|
+
# load the file
|
257
|
+
begin
|
258
|
+
require file
|
259
|
+
rescue Exception => e
|
260
|
+
logger.error("Migrator couldn't load shim file #{file} - #{e}.")
|
261
|
+
raise
|
262
|
+
end
|
263
|
+
logger.info { "Migrator loaded shim file #{file}." }
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Migrates all rows in the input.
|
268
|
+
# The required block to this method is described in {#migrate}.
|
269
|
+
def migrate_rows # :yields: target
|
270
|
+
# open an CSV output for bad records if the option is set
|
271
|
+
if @bad_rec_file then
|
272
|
+
@loader.trash = @bad_rec_file
|
273
|
+
logger.info("Unmigrated records will be written to #{File.expand_path(@bad_rec_file)}.")
|
274
|
+
end
|
275
|
+
rec_cnt = mgt_cnt = 0
|
276
|
+
logger.info { "Migrating #{@input}..." }
|
277
|
+
@loader.each do |row|
|
278
|
+
# the one-based current record number
|
279
|
+
rec_no = rec_cnt + 1
|
280
|
+
# skip if the row precedes the offset option
|
281
|
+
rec_cnt += 1 && next if rec_cnt < @offset
|
282
|
+
begin
|
283
|
+
# migrate the row
|
284
|
+
target = migrate_row(row)
|
285
|
+
# call the block on the migrated target
|
286
|
+
if target then
|
287
|
+
logger.debug { "Migrator built #{target} with the following content:\n#{target.dump}" }
|
288
|
+
yield target
|
289
|
+
end
|
290
|
+
rescue Exception => e
|
291
|
+
trace = e.backtrace.join("\n")
|
292
|
+
logger.error("Migration error on record #{rec_no} - #{e.message}:\n#{trace}")
|
293
|
+
raise unless @bad_file
|
294
|
+
end
|
295
|
+
if target then
|
296
|
+
# replace the log message below with the commented alternative to detect a memory leak
|
297
|
+
logger.debug { "Migrated record #{rec_no}." }
|
298
|
+
#memory_usage = `ps -o rss= -p #{Process.pid}`.to_f / 1024 # in megabytes
|
299
|
+
#logger.debug { "Migrated rec #{@rec_cnt}; memory usage: #{sprintf("%.1f", memory_usage)} MB." }
|
300
|
+
mgt_cnt += 1
|
301
|
+
# clear the migration state
|
302
|
+
clear(target)
|
303
|
+
else
|
304
|
+
# If there is a bad file then warn, reject and continue.
|
305
|
+
# Otherwise, bail.
|
306
|
+
if @bad_rec_file then
|
307
|
+
logger.warn("Migration not performed on record #{rec_no}.")
|
308
|
+
@loader.reject(row)
|
309
|
+
else
|
310
|
+
raise MigrationError.new("Migration not performed on record #{rec_no}.")
|
311
|
+
end
|
312
|
+
end
|
313
|
+
rec_cnt += 1
|
314
|
+
end
|
315
|
+
logger.info("Migrated #{mgt_cnt} of #{rec_cnt} records.")
|
316
|
+
end
|
317
|
+
|
318
|
+
# Clears references to objects allocated for migration of a single row into the given target.
|
319
|
+
# This method does nothing. Subclasses can override.
|
320
|
+
#
|
321
|
+
# This method is overridden by subclasses to clear the migration state to conserve memory,
|
322
|
+
# since this migrator should consume O(1) rather than O(n) memory for n migration records.
|
323
|
+
def clear(target)
|
324
|
+
end
|
325
|
+
|
326
|
+
# Imports the given CSV row into a target object.
|
327
|
+
#
|
328
|
+
# @param [{Symbol => Object}] row the input row field => value hash
|
329
|
+
# @return the migrated target object if the migration is valid, nil otherwise
|
330
|
+
def migrate_row(row) # :yields: target
|
331
|
+
# create an instance for each creatable class
|
332
|
+
created = Set.new
|
333
|
+
migrated = @creatable_classes.map { |klass| create(klass, row, created) }
|
334
|
+
# migrate each object from the input row
|
335
|
+
created.each { |obj| obj.migrate(row, migrated) }
|
336
|
+
# set the references
|
337
|
+
migrated.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
|
338
|
+
# remove invalid migrations
|
339
|
+
migrated.delete_if { |obj| not migration_valid?(obj) }
|
340
|
+
# the target object
|
341
|
+
target = migrated.detect { |obj| @target_class === obj }
|
342
|
+
if target then
|
343
|
+
logger.debug { "Migrated target #{target}." }
|
344
|
+
target
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# @param [Migratable] obj the migrated object
|
349
|
+
# @return whether the migration is successful
|
350
|
+
def migration_valid?(obj)
|
351
|
+
if obj.migration_valid? then
|
352
|
+
true
|
353
|
+
else
|
354
|
+
logger.debug { "Migrated #{obj.qp} is invalid." }
|
355
|
+
false
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Creates an instance of the given klass from the given row.
|
360
|
+
# The new klass instance and all intermediate migrated instances are added to the
|
361
|
+
# created set.
|
362
|
+
#
|
363
|
+
# @param [Class] klass
|
364
|
+
# @param [{Symbol => Object}] row the input row
|
365
|
+
# @param [<Resource>] the migrated instances for this row
|
366
|
+
# @return the new klass instance
|
367
|
+
def create(klass, row, created)
|
368
|
+
# the new object
|
369
|
+
created << obj = klass.new
|
370
|
+
# for each input header which maps to a migratable target attribute metadata path,
|
371
|
+
# set the target attribute, creating intermediate objects as needed.
|
372
|
+
@cls_paths_hash[klass].each do |path|
|
373
|
+
header = @header_map[path][klass]
|
374
|
+
# the input value
|
375
|
+
value = row[header]
|
376
|
+
next if value.nil?
|
377
|
+
# fill the reference path
|
378
|
+
ref = fill_path(obj, path[0...-1], row, created)
|
379
|
+
# set the attribute
|
380
|
+
migrate_attribute(ref, path.last, value, row)
|
381
|
+
end
|
382
|
+
logger.debug { "Migrator created #{obj}." }
|
383
|
+
obj
|
384
|
+
end
|
385
|
+
|
386
|
+
# Fills the given reference AttributeMetadata path starting at obj.
|
387
|
+
#
|
388
|
+
# @param row (see #create)
|
389
|
+
# @param created (see #create)
|
390
|
+
# @return the last domain object in the path
|
391
|
+
def fill_path(obj, path, row, created)
|
392
|
+
# create the intermediate objects as needed (or return obj if path is empty)
|
393
|
+
path.inject(obj) do |parent, attr_md|
|
394
|
+
# the referenced object
|
395
|
+
parent.send(attr_md.reader) or create_reference(parent, attr_md, row, created)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# Sets the given parent reference AttributeMetadata attr_md attribute to a new domain object.
|
400
|
+
#
|
401
|
+
# @param [Resource] parent the domain object being migrated
|
402
|
+
# @param [AttributeMetadata] attr_md the attribute being migrated
|
403
|
+
# @param row (see #create)
|
404
|
+
# @param created (see #create)
|
405
|
+
# @return the new object
|
406
|
+
def create_reference(parent, attr_md, row, created)
|
407
|
+
if attr_md.type.abstract? then
|
408
|
+
raise MigrationError.new("Cannot create #{parent.qp} #{attr_md} with abstract type #{attr_md.type}")
|
409
|
+
end
|
410
|
+
ref = attr_md.type.new
|
411
|
+
ref.migrate(row, Array::EMPTY_ARRAY)
|
412
|
+
parent.send(attr_md.writer, ref)
|
413
|
+
created << ref
|
414
|
+
logger.debug { "Migrator created #{parent.qp} #{attr_md} #{ref}." }
|
415
|
+
ref
|
416
|
+
end
|
417
|
+
|
418
|
+
# Sets the obj migratable AttributeMetadata attr_md to value from the given input row.
|
419
|
+
def migrate_attribute(obj, attr_md, value, row)
|
420
|
+
# a single value can be used for both a Numeric and a String attribute; coerce the value if necessary
|
421
|
+
# if there is a shim migrate_<attribute> method, then call it on the input value
|
422
|
+
mth = @attr_md_mgt_mth_map[attr_md]
|
423
|
+
if mth and obj.respond_to?(mth) then
|
424
|
+
value = obj.send(mth, value, row)
|
425
|
+
return if value.nil?
|
426
|
+
end
|
427
|
+
# set the attribute
|
428
|
+
begin
|
429
|
+
obj.send(attr_md.writer, value)
|
430
|
+
rescue Exception => e
|
431
|
+
raise MigrationError.new("Could not set #{obj.qp} #{attr_md} to #{value.qp} - #{e}")
|
432
|
+
end
|
433
|
+
logger.debug { "Migrated #{obj.qp} #{attr_md} to #{value}." }
|
434
|
+
end
|
435
|
+
|
436
|
+
# @param [Resource] obj the domain object to save in the database
|
437
|
+
# @return [Resource, nil] obj if the save is successful, nil otherwise
|
438
|
+
def save(obj, database)
|
439
|
+
logger.debug { "Migrator saving #{obj}..." }
|
440
|
+
database.create(obj)
|
441
|
+
logger.debug { "Migrator saved #{obj}." }
|
442
|
+
end
|
443
|
+
|
444
|
+
# @param [String] file the migration fields configuration file
|
445
|
+
# @return [{Class => {AttributeMetadata => Symbol}}] the class => path => header hash
|
446
|
+
# loaded from the configuration file
|
447
|
+
def load_field_map(file)
|
448
|
+
# load the field mapping config file
|
449
|
+
begin
|
450
|
+
config = YAML::load_file(file)
|
451
|
+
rescue
|
452
|
+
raise MigrationError.new("Could not read field map file #{file}: " + $!)
|
453
|
+
end
|
454
|
+
|
455
|
+
# collect the class => path => header entries
|
456
|
+
map = LazyHash.new { Hash.new }
|
457
|
+
config.each do |field, attr_list|
|
458
|
+
next if attr_list.blank?
|
459
|
+
# the header accessor method for the field
|
460
|
+
header = @loader.accessor(field)
|
461
|
+
raise MigrationError.new("Field defined in migration configuration not found: #{field}") if header.nil?
|
462
|
+
attr_list.split(/,\s*/).each do |path_s|
|
463
|
+
klass, path = create_attribute_path(path_s)
|
464
|
+
map[klass][path] = header
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# include the target class
|
469
|
+
map[@target_class] ||= Hash.new
|
470
|
+
|
471
|
+
# add superclass paths into subclass paths
|
472
|
+
map.each do |klass, path_hdr_hash|
|
473
|
+
map.each do |other, other_path_hdr_hash|
|
474
|
+
if klass < other then
|
475
|
+
# add, but don't replace, path => header entries from superclass
|
476
|
+
path_hdr_hash.merge!(other_path_hdr_hash) { |key, old, new| old }
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
# include only concrete classes
|
482
|
+
classes = map.enum_keys
|
483
|
+
map.delete_if do |klass, paths|
|
484
|
+
klass.abstract? or classes.any? { |other| other < klass }
|
485
|
+
end
|
486
|
+
map
|
487
|
+
end
|
488
|
+
|
489
|
+
# Returns an array of AttributeMetadata objects for the period-delimited path string path_s in the
|
490
|
+
# form _class_(._attribute_)+.
|
491
|
+
#
|
492
|
+
# Raises MigrationError if the path string is malformed or an attribute is not found.
|
493
|
+
def create_attribute_path(path_s)
|
494
|
+
names = path_s.split('.')
|
495
|
+
# if the path starts with a capitalized class name, then resolve the class.
|
496
|
+
# otherwise, the target class is the start of the path.
|
497
|
+
klass = names.first =~ /^[A-Z]/ ? @target_class.domain_module.const_get(names.shift) : @target_class
|
498
|
+
# there must be at least one attribute
|
499
|
+
if names.empty? then
|
500
|
+
raise MigrationError.new("Attribute entry in migration configuration is not in <class>.<attribute> format: #{value}")
|
501
|
+
end
|
502
|
+
# build the AttributeMetadata path
|
503
|
+
path = []
|
504
|
+
names.inject(klass) do |parent, name|
|
505
|
+
attr_md = parent.attribute_metadata(name.to_sym) rescue nil
|
506
|
+
raise MigrationError.new("Migration field mapping attribute not found: #{parent.qp}.#{name}") if attr_md.nil?
|
507
|
+
path << attr_md
|
508
|
+
attr_md.type
|
509
|
+
end
|
510
|
+
# return the starting class and AttributeMetadata path.
|
511
|
+
# note that the starting class is not necessarily the first path attribute declarer, since the
|
512
|
+
# starting class could be the concrete target class rather than an abstract declarer. this is
|
513
|
+
# important, since the class must be instantiated.
|
514
|
+
[klass, path]
|
515
|
+
end
|
516
|
+
|
517
|
+
# @return a new class => [paths] hash from the migration fields configuration map
|
518
|
+
def create_class_paths_hash(fld_map)
|
519
|
+
hash = {}
|
520
|
+
fld_map.each { |klass, path_hdr_hash| hash[klass] = path_hdr_hash.keys.to_set }
|
521
|
+
hash
|
522
|
+
end
|
523
|
+
|
524
|
+
# @return a new path => class => header hash from the migration fields configuration map
|
525
|
+
def create_header_map(fld_map)
|
526
|
+
hash = LazyHash.new { Hash.new }
|
527
|
+
fld_map.each do |klass, path_hdr_hash|
|
528
|
+
path_hdr_hash.each { |path, hdr| hash[path][klass] = hdr }
|
529
|
+
end
|
530
|
+
hash
|
531
|
+
end
|
532
|
+
end
|
533
|
+
end
|