caruby-core 1.4.1
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 +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
|