caruby-core 1.5.5 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -0
- data/History.md +5 -1
- data/lib/caruby.rb +3 -5
- data/lib/caruby/caruby-src.tar.gz +0 -0
- data/lib/caruby/database.rb +53 -69
- data/lib/caruby/database/application_service.rb +25 -0
- data/lib/caruby/database/cache.rb +60 -0
- data/lib/caruby/database/fetched_matcher.rb +52 -38
- data/lib/caruby/database/lazy_loader.rb +4 -4
- data/lib/caruby/database/operation.rb +34 -0
- data/lib/caruby/database/persistable.rb +171 -86
- data/lib/caruby/database/persistence_service.rb +32 -34
- data/lib/caruby/database/persistifier.rb +100 -43
- data/lib/caruby/database/reader.rb +107 -85
- data/lib/caruby/database/reader_template_builder.rb +60 -0
- data/lib/caruby/database/saved_matcher.rb +3 -3
- data/lib/caruby/database/sql_executor.rb +88 -17
- data/lib/caruby/database/writer.rb +213 -177
- data/lib/caruby/database/writer_template_builder.rb +334 -0
- data/lib/caruby/{util → helpers}/controlled_value.rb +0 -0
- data/lib/caruby/{util → helpers}/coordinate.rb +4 -4
- data/lib/caruby/{util → helpers}/person.rb +3 -3
- data/lib/caruby/{util → helpers}/properties.rb +7 -9
- data/lib/caruby/{util → helpers}/roman.rb +2 -2
- data/lib/caruby/{util → helpers}/version.rb +1 -1
- data/lib/caruby/json/deserializer.rb +2 -2
- data/lib/caruby/json/serializer.rb +49 -7
- data/lib/caruby/metadata.rb +30 -0
- data/lib/caruby/metadata/java_property.rb +21 -0
- data/lib/caruby/metadata/propertied.rb +191 -0
- data/lib/caruby/metadata/property.rb +22 -0
- data/lib/caruby/metadata/property_characteristics.rb +201 -0
- data/lib/caruby/migration/migratable.rb +11 -182
- data/lib/caruby/rdbi/driver/jdbc.rb +446 -0
- data/lib/caruby/resource.rb +20 -823
- data/lib/caruby/version.rb +1 -1
- data/test/lib/caruby/database/cache_test.rb +54 -0
- data/test/lib/caruby/{util → helpers}/controlled_value_test.rb +3 -5
- data/test/lib/caruby/{util → helpers}/person_test.rb +4 -6
- data/test/lib/caruby/helpers/properties_test.rb +34 -0
- data/test/lib/caruby/{util → helpers}/roman_test.rb +2 -3
- data/test/lib/caruby/{util → helpers}/version_test.rb +2 -3
- data/test/lib/helper.rb +7 -0
- metadata +161 -214
- data/lib/caruby/cli/application.rb +0 -36
- data/lib/caruby/cli/command.rb +0 -202
- data/lib/caruby/csv/csv_mapper.rb +0 -159
- data/lib/caruby/csv/csvio.rb +0 -203
- data/lib/caruby/database/search_template_builder.rb +0 -56
- data/lib/caruby/database/store_template_builder.rb +0 -278
- data/lib/caruby/domain.rb +0 -193
- data/lib/caruby/domain/attribute.rb +0 -584
- data/lib/caruby/domain/attributes.rb +0 -628
- data/lib/caruby/domain/dependency.rb +0 -225
- data/lib/caruby/domain/id_alias.rb +0 -22
- data/lib/caruby/domain/importer.rb +0 -183
- data/lib/caruby/domain/introspection.rb +0 -176
- data/lib/caruby/domain/inverse.rb +0 -172
- data/lib/caruby/domain/inversible.rb +0 -90
- data/lib/caruby/domain/java_attribute.rb +0 -173
- data/lib/caruby/domain/merge.rb +0 -185
- data/lib/caruby/domain/metadata.rb +0 -142
- data/lib/caruby/domain/mixin.rb +0 -35
- data/lib/caruby/domain/properties.rb +0 -95
- data/lib/caruby/domain/reference_visitor.rb +0 -428
- data/lib/caruby/domain/uniquify.rb +0 -50
- data/lib/caruby/import/java.rb +0 -387
- data/lib/caruby/migration/migrator.rb +0 -918
- data/lib/caruby/migration/resource_module.rb +0 -9
- data/lib/caruby/migration/uniquify.rb +0 -17
- data/lib/caruby/util/attribute_path.rb +0 -44
- data/lib/caruby/util/cache.rb +0 -56
- data/lib/caruby/util/class.rb +0 -149
- data/lib/caruby/util/collection.rb +0 -1152
- data/lib/caruby/util/domain_extent.rb +0 -46
- data/lib/caruby/util/file_separator.rb +0 -65
- data/lib/caruby/util/inflector.rb +0 -27
- data/lib/caruby/util/log.rb +0 -95
- data/lib/caruby/util/math.rb +0 -12
- data/lib/caruby/util/merge.rb +0 -59
- data/lib/caruby/util/module.rb +0 -18
- data/lib/caruby/util/options.rb +0 -97
- data/lib/caruby/util/partial_order.rb +0 -35
- data/lib/caruby/util/pretty_print.rb +0 -204
- data/lib/caruby/util/stopwatch.rb +0 -74
- data/lib/caruby/util/topological_sync_enumerator.rb +0 -62
- data/lib/caruby/util/transitive_closure.rb +0 -55
- data/lib/caruby/util/tree.rb +0 -48
- data/lib/caruby/util/trie.rb +0 -37
- data/lib/caruby/util/uniquifier.rb +0 -30
- data/lib/caruby/util/validation.rb +0 -20
- data/lib/caruby/util/visitor.rb +0 -365
- data/lib/caruby/util/weak_hash.rb +0 -36
- data/test/lib/caruby/csv/csv_mapper_test.rb +0 -40
- data/test/lib/caruby/csv/csvio_test.rb +0 -69
- data/test/lib/caruby/database/persistable_test.rb +0 -92
- data/test/lib/caruby/domain/domain_test.rb +0 -112
- data/test/lib/caruby/domain/inversible_test.rb +0 -99
- data/test/lib/caruby/domain/reference_visitor_test.rb +0 -130
- data/test/lib/caruby/import/java_test.rb +0 -80
- data/test/lib/caruby/import/mixed_case_test.rb +0 -14
- data/test/lib/caruby/migration/test_case.rb +0 -102
- data/test/lib/caruby/test_case.rb +0 -230
- data/test/lib/caruby/util/cache_test.rb +0 -23
- data/test/lib/caruby/util/class_test.rb +0 -61
- data/test/lib/caruby/util/collection_test.rb +0 -398
- data/test/lib/caruby/util/command_test.rb +0 -55
- data/test/lib/caruby/util/domain_extent_test.rb +0 -60
- data/test/lib/caruby/util/file_separator_test.rb +0 -30
- data/test/lib/caruby/util/inflector_test.rb +0 -12
- data/test/lib/caruby/util/lazy_hash_test.rb +0 -34
- data/test/lib/caruby/util/merge_test.rb +0 -83
- data/test/lib/caruby/util/module_test.rb +0 -25
- data/test/lib/caruby/util/options_test.rb +0 -59
- data/test/lib/caruby/util/partial_order_test.rb +0 -42
- data/test/lib/caruby/util/pretty_print_test.rb +0 -85
- data/test/lib/caruby/util/properties_test.rb +0 -50
- data/test/lib/caruby/util/stopwatch_test.rb +0 -18
- data/test/lib/caruby/util/topological_sync_enumerator_test.rb +0 -69
- data/test/lib/caruby/util/transitive_closure_test.rb +0 -67
- data/test/lib/caruby/util/tree_test.rb +0 -23
- data/test/lib/caruby/util/trie_test.rb +0 -14
- data/test/lib/caruby/util/visitor_test.rb +0 -278
- data/test/lib/caruby/util/weak_hash_test.rb +0 -45
- data/test/lib/examples/clinical_trials/migration/migration_test.rb +0 -58
- data/test/lib/examples/clinical_trials/migration/test_case.rb +0 -38
@@ -1,50 +0,0 @@
|
|
1
|
-
require 'singleton'
|
2
|
-
require 'caruby/util/uniquifier'
|
3
|
-
require 'caruby/util/collection'
|
4
|
-
|
5
|
-
module CaRuby
|
6
|
-
module Resource
|
7
|
-
# The Unique mix-in makes values unique within the scope of a Resource class.
|
8
|
-
module Unique
|
9
|
-
# Makes the given String value unique in the context of this object's class.
|
10
|
-
# @return nil if value is nil
|
11
|
-
# Raises TypeError if value is neither nil nor a String.
|
12
|
-
def uniquify_value(value)
|
13
|
-
unless String === value or value.nil? then
|
14
|
-
raise TypeError.new("Cannot uniquify #{qp} non-String value #{value}")
|
15
|
-
end
|
16
|
-
ResourceUniquifier.instance.uniquify(self, value)
|
17
|
-
end
|
18
|
-
|
19
|
-
# Makes the secondary key unique by replacing each String key attribute value
|
20
|
-
# with a unique value.
|
21
|
-
def uniquify
|
22
|
-
self.class.secondary_key_attributes.each do |attr|
|
23
|
-
oldval = send(attr)
|
24
|
-
next unless String === oldval
|
25
|
-
newval = uniquify_value(oldval)
|
26
|
-
set_attribute(attr, newval)
|
27
|
-
logger.debug { "Reset #{qp} #{attr} from #{oldval} to unique value #{newval}." }
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
# The ResourceUniquifier singleton makes Resource instance attribute values unique.
|
34
|
-
class ResourceUniquifier
|
35
|
-
include Singleton
|
36
|
-
|
37
|
-
def initialize
|
38
|
-
@cache = LazyHash.new { Hash.new }
|
39
|
-
end
|
40
|
-
|
41
|
-
# Makes the obj attribute value unique, or returns nil if value is nil.
|
42
|
-
def uniquify(obj, value)
|
43
|
-
@cache[obj.class][value] ||= value.uniquify if value
|
44
|
-
end
|
45
|
-
|
46
|
-
def clear
|
47
|
-
@cache.clear
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
data/lib/caruby/import/java.rb
DELETED
@@ -1,387 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# Include file to set up the classpath and logger.
|
3
|
-
#
|
4
|
-
|
5
|
-
# The jRuby Java bridge
|
6
|
-
require 'java'
|
7
|
-
require 'ftools'
|
8
|
-
require 'date'
|
9
|
-
|
10
|
-
require 'caruby/util/class'
|
11
|
-
require 'caruby/util/log'
|
12
|
-
require 'caruby/util/inflector'
|
13
|
-
require 'caruby/util/collection'
|
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
|
-
|
26
|
-
# Adds the directories in the given path and all Java jar files contained in the directories
|
27
|
-
# to the execution classpath.
|
28
|
-
#
|
29
|
-
# @param [String] path the colon or semi-colon separated directories
|
30
|
-
def self.add_path(path)
|
31
|
-
# the path separator
|
32
|
-
sep = path[WINDOWS_PATH_SEP] ? WINDOWS_PATH_SEP : UNIX_PATH_SEP
|
33
|
-
# the path directories
|
34
|
-
dirs = path.split(sep).map { |dir| File.expand_path(dir) }
|
35
|
-
# Add all jars found anywhere within the directories to the the classpath.
|
36
|
-
add_jars(*dirs)
|
37
|
-
# Add the directories to the the classpath.
|
38
|
-
dirs.each { |dir| add_to_classpath(dir) }
|
39
|
-
end
|
40
|
-
|
41
|
-
# Adds the jars in the directories to the execution class path.
|
42
|
-
#
|
43
|
-
# @param [<String>] directories the directories containing jars to add
|
44
|
-
def self.add_jars(*directories)
|
45
|
-
directories.each do |dir|
|
46
|
-
Dir[File.join(dir , "**", "*.jar")].each { |jar| add_to_classpath(jar) }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Adds the given jar file or directory to the classpath.
|
51
|
-
#
|
52
|
-
# @param [String] file the jar file or directory to add
|
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
|
58
|
-
if file =~ /.jar$/ then
|
59
|
-
# require is preferred to classpath append for a jar file
|
60
|
-
require file
|
61
|
-
else
|
62
|
-
# A directory must end in a slash since JRuby uses an URLClassLoader.
|
63
|
-
if File.directory?(file) and not file =~ /\/$/ then file = file + '/' end
|
64
|
-
$CLASSPATH << file
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
module JavaUtil
|
69
|
-
# Aliases Java Collection methods with the standard Ruby Set counterpart, e.g. +delete+ for +remove+.
|
70
|
-
module Collection
|
71
|
-
def to_a
|
72
|
-
inject(Array.new) { |array, item| array << item }
|
73
|
-
end
|
74
|
-
|
75
|
-
# Removes the given item from this collection.
|
76
|
-
def delete(item)
|
77
|
-
# can't alias delete to remove, since a Java interface doesn't implement any methods
|
78
|
-
remove(item)
|
79
|
-
end
|
80
|
-
|
81
|
-
# Removes the items from this collection for which the block given to this method returns a non-nil, non-false value.
|
82
|
-
def delete_if
|
83
|
-
removeAll(select { |item| yield item })
|
84
|
-
self
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
# Aliases Java List methods with the standard Ruby Array counterpart, e.g. +merge+ for +addAll+.
|
89
|
-
module List
|
90
|
-
# Returns whether this List has the same content as the other Java List or Ruby Array.
|
91
|
-
def ==(other)
|
92
|
-
Array === other ? to_a == other : equals(other)
|
93
|
-
end
|
94
|
-
|
95
|
-
# Removes the given item from this collection.
|
96
|
-
def delete(item)
|
97
|
-
remove(item)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
module Map
|
102
|
-
# Returns whether this Set has the same content as the other Java Map or Ruby Hash.
|
103
|
-
def ==(other)
|
104
|
-
::Hash === other ? (size == other.size and other.all? { |key, value| get(key) == value }) : equals(other)
|
105
|
-
end
|
106
|
-
|
107
|
-
# Merges the other Java Map or Ruby Hash into this Map. Returns this modified Map.
|
108
|
-
#
|
109
|
-
# If a block is given to this method, then the block determines the mapped value
|
110
|
-
# as specified in the Ruby Hash merge method documentation.
|
111
|
-
def merge(other)
|
112
|
-
other.each do |key, value|
|
113
|
-
value = yield(key, get(key), value) if block_given? and containsKey(key)
|
114
|
-
put(key, value)
|
115
|
-
end
|
116
|
-
self
|
117
|
-
end
|
118
|
-
|
119
|
-
alias :merge! :merge
|
120
|
-
end
|
121
|
-
|
122
|
-
module Set
|
123
|
-
# Returns whether this Set has the same content as the other Java Set or Ruby Set.
|
124
|
-
def ==(other)
|
125
|
-
::Set === other ? (size == other.size and all? { |item| other.include?(item) }) : equals(other)
|
126
|
-
end
|
127
|
-
|
128
|
-
# Merges the other Enumerable into this Set. Returns this modified Set.
|
129
|
-
#
|
130
|
-
# This method conforms to the Ruby Set merge contract rather than the Ruby List and Hash
|
131
|
-
# merge contract. Ruby Set merge modifies the Set in-place, whereas Ruby List and Hash
|
132
|
-
# merge return a new collection.
|
133
|
-
def merge(other)
|
134
|
-
return self if other.nil?
|
135
|
-
raise ArgumentError.new("Merge argument must be enumerable: #{other}") unless Enumerable === other
|
136
|
-
other.each { |item| self << item }
|
137
|
-
self
|
138
|
-
end
|
139
|
-
|
140
|
-
alias :merge! :merge
|
141
|
-
end
|
142
|
-
|
143
|
-
class HashSet
|
144
|
-
alias :base__clear :clear
|
145
|
-
private :base__clear
|
146
|
-
def clear
|
147
|
-
base__clear
|
148
|
-
self
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
class TreeSet
|
153
|
-
alias :base__first :first
|
154
|
-
private :base__first
|
155
|
-
# Fixes the jRuby {TreeSet#first} to return nil on an empty set rather than raise a Java exception.
|
156
|
-
def first
|
157
|
-
empty? ? nil : base__first
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
class ArrayList
|
162
|
-
alias :base__clear :clear
|
163
|
-
private :base__clear
|
164
|
-
def clear
|
165
|
-
base__clear
|
166
|
-
self
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
class Date
|
171
|
-
# The millisecond-to-day conversion factor.
|
172
|
-
MILLIS_PER_DAY = (60 * 60 * 1000) * 24
|
173
|
-
|
174
|
-
# Converts this Java Date to a Ruby DateTime.
|
175
|
-
#
|
176
|
-
# @quirk caTissue Bug #165: API CPR create date validation is time zone dependent.
|
177
|
-
# Since Java Date accounts for DST and Ruby DateTime doesn't, this method makes the
|
178
|
-
# DST adjustment by subtracting a compensatory one-hour DST offset from the
|
179
|
-
# Java Date time zone offset and using that to set the DateTime offset.
|
180
|
-
# This ensures that Date conversion is idempotent, i.e.
|
181
|
-
# date.to_ruby_date().to_java_date == date
|
182
|
-
#
|
183
|
-
# However, there can be adverse consequences for an application that assumes that the
|
184
|
-
# client time zone is the same as the server time zone, as described in caTissue
|
185
|
-
# Bug #165.
|
186
|
-
#
|
187
|
-
# @return [DateTime] the Ruby date
|
188
|
-
def to_ruby_date
|
189
|
-
calendar = java.util.Calendar.instance
|
190
|
-
calendar.setTime(self)
|
191
|
-
secs = calendar.timeInMillis.to_f / 1000
|
192
|
-
# millis since epoch
|
193
|
-
time = Time.at(secs)
|
194
|
-
# convert UTC timezone millisecond offset to Rational fraction of a day
|
195
|
-
offset_millis = calendar.timeZone.getOffset(calendar.timeInMillis).to_f
|
196
|
-
if offset_millis.zero? then
|
197
|
-
offset = 0
|
198
|
-
else
|
199
|
-
offset_days = offset_millis / MILLIS_PER_DAY
|
200
|
-
offset_fraction = 1 / offset_days
|
201
|
-
offset = Rational(1, offset_fraction)
|
202
|
-
end
|
203
|
-
# convert to DateTime
|
204
|
-
DateTime.civil(time.year, time.mon, time.day, time.hour, time.min, time.sec, offset)
|
205
|
-
end
|
206
|
-
|
207
|
-
# Converts a Ruby Date or DateTime to a Java Date.
|
208
|
-
#
|
209
|
-
# @param [::Date, DateTime] date the Ruby date
|
210
|
-
# @return [Date] the Java date
|
211
|
-
def self.from_ruby_date(date)
|
212
|
-
return if date.nil?
|
213
|
-
# DateTime has time attributes, Date doesn't
|
214
|
-
if DateTime === date then
|
215
|
-
hour, min, sec = date.hour, date.min, date.sec
|
216
|
-
else
|
217
|
-
hour = min = sec = 0
|
218
|
-
end
|
219
|
-
# the Ruby time
|
220
|
-
rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, nil, nil)
|
221
|
-
# millis since epoch
|
222
|
-
millis = (rtime.to_f * 1000).truncate
|
223
|
-
# the Java date factory
|
224
|
-
calendar = java.util.Calendar.instance
|
225
|
-
calendar.setTimeInMillis(millis)
|
226
|
-
jtime = calendar.getTime
|
227
|
-
# the daylight time flag
|
228
|
-
isdt = calendar.timeZone.inDaylightTime(jtime)
|
229
|
-
return jtime unless isdt
|
230
|
-
# adjust the Ruby time for DST
|
231
|
-
rtime = Time.local(sec, min, hour, date.day, date.mon, date.year, nil, nil, isdt, nil)
|
232
|
-
millis = (rtime.to_f * 1000).truncate
|
233
|
-
calendar.setTimeInMillis(millis)
|
234
|
-
calendar.getTime
|
235
|
-
end
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
|
-
def self.now
|
240
|
-
JavaUtil::Date.from_ruby_date(DateTime.now)
|
241
|
-
end
|
242
|
-
|
243
|
-
# @param [Class, String] the JRuby class or the full Java class name
|
244
|
-
# @return (String, String] the package and base for the given name
|
245
|
-
def self.split_class_name(name_or_class)
|
246
|
-
name = Class === name_or_class ? name_or_class.java_class.name : name_or_class
|
247
|
-
match = NAME_SPLITTER_REGEX.match(name)
|
248
|
-
match ? match.captures : [nil, name]
|
249
|
-
end
|
250
|
-
|
251
|
-
private
|
252
|
-
|
253
|
-
NAME_SPLITTER_REGEX = /^([\w.]+)\.(\w+)$/
|
254
|
-
end
|
255
|
-
|
256
|
-
class Class
|
257
|
-
# Returns whether this is a Java wrapper class.
|
258
|
-
def java_class?
|
259
|
-
method_defined?(:java_class)
|
260
|
-
end
|
261
|
-
|
262
|
-
# Returns the Ruby class for the given class, as follows:
|
263
|
-
# * If the given class is already a Ruby class, then return the class.
|
264
|
-
# * If the class argument is a Java class or a Java class name, then
|
265
|
-
# the Ruby class is the JRuby wrapper for the Java class.
|
266
|
-
#
|
267
|
-
# @param [Class, String] class_or_name the class or class name
|
268
|
-
# @return [Class] the corresponding Ruby class
|
269
|
-
def self.to_ruby(class_or_name)
|
270
|
-
case class_or_name
|
271
|
-
when Class then class_or_name
|
272
|
-
when String then eval to_ruby_name(class_or_name)
|
273
|
-
else to_ruby(class_or_name.name)
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
|
-
# @return [Boolean] whether this is a wrapper for an abstract Java class
|
278
|
-
def abstract?
|
279
|
-
java_class? and Java::JavaLangReflect::Modifier.isAbstract(java_class.modifiers)
|
280
|
-
end
|
281
|
-
|
282
|
-
# Returns whether the given PropertyDescriptor pd corresponds to a transient field in this class, or nil if there is no such field.
|
283
|
-
def transient?(pd)
|
284
|
-
begin
|
285
|
-
field = java_class.declared_field(pd.name)
|
286
|
-
rescue Exception
|
287
|
-
# should occur only if a property is not a field; not an error
|
288
|
-
return
|
289
|
-
end
|
290
|
-
Java::JavaLangReflect::Modifier.isTransient(field.modifiers) if field
|
291
|
-
end
|
292
|
-
|
293
|
-
# Returns this class's readable and writable Java PropertyDescriptors, or an empty Array if none.
|
294
|
-
# If the hierarchy flag is set to +false+, then only this class's properties
|
295
|
-
# will be introspected.
|
296
|
-
def java_properties(hierarchy=true)
|
297
|
-
return Array::EMPTY_ARRAY unless java_class?
|
298
|
-
info = hierarchy ? Java::JavaBeans::Introspector.getBeanInfo(java_class) : Java::JavaBeans::Introspector.getBeanInfo(java_class, java_class.superclass)
|
299
|
-
info.propertyDescriptors.select { |pd| pd.write_method and property_read_method(pd) }
|
300
|
-
end
|
301
|
-
|
302
|
-
# Redefines the reserved method corresponeding to the given Java property descriptor pd
|
303
|
-
# back to the Object implementation, if necessary.
|
304
|
-
# If both this class and Object define a method with the property name,
|
305
|
-
# then a new method is defined with the same body as the previous method.
|
306
|
-
# Returns the new method symbol, or nil if name_or_symbol is not an occluded
|
307
|
-
# Object instance method.
|
308
|
-
#
|
309
|
-
# This method undoes the jRuby clobbering of Object methods by Java property method
|
310
|
-
# wrappers. The method is renamed as follows:
|
311
|
-
# * +id+ is changed to :identifier
|
312
|
-
# * +type+ is prefixed by the underscore subject class name, e.g. +Specimen.type => :specimen_type+,
|
313
|
-
# If the property name is +type+ and the subject class name ends in 'Type', then the attribute
|
314
|
-
# symbol is the underscore subject class name, e.g. +HistologicType.type => :histologic_type+.
|
315
|
-
#
|
316
|
-
# Raises ArgumentError if symbol is not an Object method.
|
317
|
-
def unocclude_reserved_method(pd)
|
318
|
-
oldname = pd.name.underscore
|
319
|
-
return unless OBJ_INST_MTHDS.include?(oldname)
|
320
|
-
oldsym = oldname.to_sym
|
321
|
-
undeprecated = case oldsym
|
322
|
-
when :id then :object_id
|
323
|
-
when :type then :class
|
324
|
-
else oldsym
|
325
|
-
end
|
326
|
-
rsvd_mth = Object.instance_method(undeprecated)
|
327
|
-
base = self.qp.underscore
|
328
|
-
newname = if oldname == 'id' then
|
329
|
-
'identifier'
|
330
|
-
elsif base[-oldname.length..-1] == oldname then
|
331
|
-
base
|
332
|
-
else
|
333
|
-
"#{base}_#{oldname}"
|
334
|
-
end
|
335
|
-
newsym = newname.to_sym
|
336
|
-
rdr = property_read_method(pd).name.to_sym
|
337
|
-
alias_method(newsym, rdr)
|
338
|
-
# alias the writers
|
339
|
-
wtr = pd.write_method.name.to_sym
|
340
|
-
alias_method("#{newsym}=".to_sym, wtr)
|
341
|
-
# alias a camel-case Java-style method if necessary
|
342
|
-
altname = newname.camelize
|
343
|
-
unless altname == newname then
|
344
|
-
alias_method(altname.to_sym, oldsym)
|
345
|
-
alias_method("#{altname}=".to_sym, wtr)
|
346
|
-
end
|
347
|
-
# restore the old method to Object
|
348
|
-
define_method(oldsym) { |*args| rsvd_mth.bind(self).call(*args) }
|
349
|
-
newsym
|
350
|
-
end
|
351
|
-
|
352
|
-
# @quirk caCORE java.lang.Boolean is<name> is not introspected as a read method, since the type
|
353
|
-
# must be primitive, i.e. `boolean is`<name>.
|
354
|
-
#
|
355
|
-
# @return [Symbol] the property descriptor pd introspected or discovered Java read Method
|
356
|
-
def property_read_method(pd)
|
357
|
-
return pd.read_method if pd.read_method
|
358
|
-
return unless pd.get_property_type == Java::JavaLang::Boolean.java_class
|
359
|
-
rdr = java_class.java_method("is#{pd.name.capitalize_first}") rescue nil
|
360
|
-
logger.debug { "Discovered #{qp} #{pd.name} property non-introspected reader method #{rdr.name}." } if rdr
|
361
|
-
rdr
|
362
|
-
end
|
363
|
-
|
364
|
-
private
|
365
|
-
|
366
|
-
OBJ_INST_MTHDS = Object.instance_methods
|
367
|
-
|
368
|
-
# @param [String] jname the fully-qualified Java class or interface name
|
369
|
-
# @return [String] the JRuby class or module name
|
370
|
-
# #example
|
371
|
-
# Java.to_ruby_class_name('com.test.Sample') #=> Java::ComTest::Sample
|
372
|
-
def self.to_ruby_name(jname)
|
373
|
-
path = jname.split('.')
|
374
|
-
return "Java::#{jname}" if path.size == 1
|
375
|
-
cname = path[-1]
|
376
|
-
pkg = path[0...-1].map { |s| s.capitalize_first }.join
|
377
|
-
"Java::#{pkg}::#{cname}"
|
378
|
-
end
|
379
|
-
end
|
380
|
-
|
381
|
-
class Array
|
382
|
-
alias :equal__base :==
|
383
|
-
# Overrides the standard == to compare a Java List with a Ruby Array.
|
384
|
-
def ==(other)
|
385
|
-
Java::JavaUtil::List === other ? other == self : equal__base(other)
|
386
|
-
end
|
387
|
-
end
|
@@ -1,918 +0,0 @@
|
|
1
|
-
# load the required gems
|
2
|
-
require 'rubygems'
|
3
|
-
|
4
|
-
# the Units of Measurement 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/migratable'
|
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 required application {Database}
|
31
|
-
# @option opts [String] :target required target domain class
|
32
|
-
# @option opts [<String>, String] :mapping required input field => caTissue attribute mapping file(s)
|
33
|
-
# @option opts [<String>, String] :defaults optional caTissue attribute => value default mapping file(s)
|
34
|
-
# @option opts [<String>, String] :filters optional caTissue attribute input value => caTissue value filter file(s)
|
35
|
-
# @option opts [String] :input required source file to migrate
|
36
|
-
# @option opts [<String>, String] :shims optional shim file(s) to load
|
37
|
-
# @option opts [String] :unique ensures that migrated objects which include the {Resource::Unique}
|
38
|
-
# @option opts [String] :create optional flag indicating that existing target objects are ignored
|
39
|
-
# @option opts [String] :bad optional invalid record file
|
40
|
-
# @option opts [Integer] :offset zero-based starting source record number to process (default 0)
|
41
|
-
# @option opts [Boolean] :quiet suppress output messages
|
42
|
-
# @option opts [Boolean] :verbose print progress
|
43
|
-
def initialize(opts)
|
44
|
-
@rec_cnt = 0
|
45
|
-
parse_options(opts)
|
46
|
-
build
|
47
|
-
end
|
48
|
-
|
49
|
-
# Imports this migrator's file into the database with the given connect options.
|
50
|
-
# This method creates or updates the domain objects mapped from the import source.
|
51
|
-
# If a block is given to this method, then the block is called on each stored
|
52
|
-
# migration target object.
|
53
|
-
#
|
54
|
-
# If the +:create+ option is set, then an input record for a target object which already
|
55
|
-
# exists in the database is noted in a debug log message and ignored rather than updated.
|
56
|
-
#
|
57
|
-
# @yield [target] operation performed on the migration target
|
58
|
-
# @yieldparam [Resource] target the migrated target domain object
|
59
|
-
def migrate_to_database(&block)
|
60
|
-
# migrate with save
|
61
|
-
tm = Stopwatch.measure { execute_save(&block) }.elapsed
|
62
|
-
logger.debug { format_migration_time_log_message(tm) }
|
63
|
-
end
|
64
|
-
|
65
|
-
# Imports this migrator's CSV file and calls the required block on each migrated target
|
66
|
-
# domain object.
|
67
|
-
#
|
68
|
-
# @yield [target] operation performed on the migration target
|
69
|
-
# @yieldparam [Resource] target the migrated target domain object
|
70
|
-
def migrate(&block)
|
71
|
-
raise MigrationError.new("The caRuby Migrator migrate block is missing") unless block_given?
|
72
|
-
migrate_rows(&block)
|
73
|
-
end
|
74
|
-
|
75
|
-
alias :each :migrate
|
76
|
-
|
77
|
-
private
|
78
|
-
|
79
|
-
REGEXP_PAT = /^\/(.*[^\\])\/([inx]+)?$/
|
80
|
-
|
81
|
-
# Class {#migrate} with a {#save} block.
|
82
|
-
def execute_save
|
83
|
-
if @database.nil? then
|
84
|
-
raise MigrationError.new("Migrator cannot save records since the database option was not specified.")
|
85
|
-
end
|
86
|
-
@database.open do |db|
|
87
|
-
migrate do |target|
|
88
|
-
save(target, db)
|
89
|
-
yield target if block_given?
|
90
|
-
db.clear
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
# @return a log message String for the given migration time in seconds
|
96
|
-
def format_migration_time_log_message(time)
|
97
|
-
# the database execution time
|
98
|
-
dt = @database.execution_time
|
99
|
-
if time > 120 then
|
100
|
-
time /= 60
|
101
|
-
dt /= 60
|
102
|
-
unit = "minutes"
|
103
|
-
else
|
104
|
-
unit = "seconds"
|
105
|
-
end
|
106
|
-
"Migration took #{'%.2f' % time} #{unit}, of which #{'%.2f' % dt} were database operations."
|
107
|
-
end
|
108
|
-
|
109
|
-
def parse_options(opts)
|
110
|
-
@fld_map_files = opts[:mapping]
|
111
|
-
raise MigrationError.new("Migrator missing required field mapping file parameter") if @fld_map_files.nil?
|
112
|
-
@def_files = opts[:defaults]
|
113
|
-
@filter_files = opts[:filters]
|
114
|
-
@shims = opts[:shims] ||= []
|
115
|
-
@offset = opts[:offset] ||= 0
|
116
|
-
@input = Options.get(:input, opts)
|
117
|
-
raise MigrationError.new("Migrator missing required source file parameter") if @input.nil?
|
118
|
-
@database = opts[:database]
|
119
|
-
@target_class = opts[:target]
|
120
|
-
raise MigrationError.new("Migrator missing required target class parameter") if @target_class.nil?
|
121
|
-
@bad_rec_file = opts[:bad]
|
122
|
-
@create = opts[:create]
|
123
|
-
logger.info("Migration options: #{printable_options(opts).pp_s}.")
|
124
|
-
# flag indicating whether to print a progress monitor
|
125
|
-
@print_progress = opts[:verbose]
|
126
|
-
end
|
127
|
-
|
128
|
-
def printable_options(opts)
|
129
|
-
popts = opts.reject { |option, value| value.nil_or_empty? }
|
130
|
-
# The target class should be a simple class name rather than the class metadata.
|
131
|
-
popts[:target] = popts[:target].qp if popts.has_key?(:target)
|
132
|
-
popts
|
133
|
-
end
|
134
|
-
|
135
|
-
def build
|
136
|
-
# the current source class => instance map
|
137
|
-
raise MigrationError.new("No file to migrate") if @input.nil?
|
138
|
-
|
139
|
-
# make a CSV loader which only converts input fields corresponding to non-String attributes
|
140
|
-
@loader = CsvIO.new(@input, &method(:convert))
|
141
|
-
logger.debug { "Migration data input file #{@input} headers: #{@loader.headers.qp}" }
|
142
|
-
|
143
|
-
# add shim modifiers
|
144
|
-
load_shims(@shims)
|
145
|
-
|
146
|
-
# create the class => path => default value hash
|
147
|
-
@def_hash = @def_files ? load_defaults_files(@def_files) : {}
|
148
|
-
# create the class => path => default value hash
|
149
|
-
@filter_hash = @filter_files ? load_filter_files(@filter_files) : {}
|
150
|
-
# create the class => path => header hash
|
151
|
-
fld_map = load_field_map_files(@fld_map_files)
|
152
|
-
# create the class => paths hash
|
153
|
-
@cls_paths_hash = create_class_paths_hash(fld_map, @def_hash)
|
154
|
-
# create the path => class => header hash
|
155
|
-
@header_map = create_header_map(fld_map)
|
156
|
-
# add missing owner classes (copy the keys rather than using each_key since the hash is updated)
|
157
|
-
@owners = Set.new
|
158
|
-
@cls_paths_hash.keys.each { |klass| add_owners(klass) }
|
159
|
-
# order the creatable classes by dependency, owners first, to smooth the migration process
|
160
|
-
@creatable_classes = @cls_paths_hash.keys.sort! { |klass, other| other.depends_on?(klass) ? -1 : (klass.depends_on?(other) ? 1 : 0) }
|
161
|
-
@creatable_classes.each do |klass|
|
162
|
-
if klass.abstract? then
|
163
|
-
raise MigrationError.new("Migrator cannot create the abstract class #{klass}; specify a subclass instead in the mapping file.")
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
# print the maps
|
168
|
-
print_hash = LazyHash.new { Hash.new }
|
169
|
-
@cls_paths_hash.each do |klass, paths|
|
170
|
-
print_hash[klass.qp] = paths.map { |path| {path.map { |attr_md| attr_md.to_sym }.join('.') => @header_map[path][klass] } }
|
171
|
-
end
|
172
|
-
logger.info { "Migration paths:\n#{print_hash.pp_s}" }
|
173
|
-
logger.info { "Migration creatable classes: #{@creatable_classes.qp}." }
|
174
|
-
unless @def_hash.empty? then logger.info { "Migration defaults: #{@def_hash.qp}." } end
|
175
|
-
|
176
|
-
# the class => attribute migration methods hash
|
177
|
-
create_migration_method_hashes
|
178
|
-
|
179
|
-
# Collect the String input fields for the custom CSVLoader converter.
|
180
|
-
@nonstring_headers = Set.new
|
181
|
-
logger.info("Migration attributes:")
|
182
|
-
@header_map.each do |path, cls_hdr_hash|
|
183
|
-
attr_md = path.last
|
184
|
-
cls_hdr_hash.each do |klass, hdr|
|
185
|
-
type_s = attr_md.type ? attr_md.type.qp : 'Object'
|
186
|
-
logger.info(" #{hdr} => #{klass.qp}.#{path.join('.')} (#{type_s})")
|
187
|
-
end
|
188
|
-
@nonstring_headers.merge!(cls_hdr_hash.values) if attr_md.type != Java::JavaLang::String
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
# Converts the given input field value as follows:
|
193
|
-
# * if the info header is a String field, then return the value unchanged
|
194
|
-
# * otherwise, if the value is a case-insensitive match for +true+ or +false+, then convert
|
195
|
-
# the value to the respective Boolean
|
196
|
-
# * otherwise, return nil which will delegate to the generic CsvIO converter
|
197
|
-
# @param (see CsvIO#convert)
|
198
|
-
# @yield (see CsvIO#convert)
|
199
|
-
def convert(value, info)
|
200
|
-
@nonstring_headers.include?(info.header) ? convert_boolean(value) : value
|
201
|
-
end
|
202
|
-
|
203
|
-
# @param [String] value the input value
|
204
|
-
# @return [Boolean, nil] the corresponding boolean, or nil if none
|
205
|
-
def convert_boolean(value)
|
206
|
-
case value
|
207
|
-
when /true/i then true
|
208
|
-
when /false/i then false
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
# Adds missing owner classes to the migration class path hash (with empty paths)
|
213
|
-
# for the the given migration class.
|
214
|
-
#
|
215
|
-
# @param [Class] klass the migration class
|
216
|
-
def add_owners(klass)
|
217
|
-
owner = missing_owner_for(klass) || return
|
218
|
-
logger.debug { "Migrator adding #{klass.qp} owner #{owner}" }
|
219
|
-
@owners << owner
|
220
|
-
@cls_paths_hash[owner] = Array::EMPTY_ARRAY
|
221
|
-
add_owners(owner)
|
222
|
-
end
|
223
|
-
|
224
|
-
# @param [Class] klass the migration class
|
225
|
-
# @return [Class, nil] the missing class owner, if any
|
226
|
-
def missing_owner_for(klass)
|
227
|
-
# check for an owner among the current migration classes
|
228
|
-
return if klass.owners.any? do |owner|
|
229
|
-
@cls_paths_hash.detect_key { |other| other <= owner }
|
230
|
-
end
|
231
|
-
# find the first non-abstract candidate owner
|
232
|
-
klass.owners.detect { |owner| not owner.abstract? }
|
233
|
-
end
|
234
|
-
|
235
|
-
# Creates the class => +migrate_+_<attribute>_ hash for the given klasses.
|
236
|
-
def create_migration_method_hashes
|
237
|
-
# the class => attribute => migration filter hash
|
238
|
-
@attr_flt_hash = {}
|
239
|
-
customizable_class_attributes.each do |klass, attr_mds|
|
240
|
-
flts = migration_filters(klass, attr_mds) || next
|
241
|
-
@attr_flt_hash[klass] = flts
|
242
|
-
end
|
243
|
-
|
244
|
-
# print the migration shim methods
|
245
|
-
unless @attr_flt_hash.empty? then
|
246
|
-
printer_hash = LazyHash.new { Array.new }
|
247
|
-
@attr_flt_hash.each do |klass, hash|
|
248
|
-
mths = hash.values.select { |flt| Symbol === flt }
|
249
|
-
printer_hash[klass.qp] = mths unless mths.empty?
|
250
|
-
end
|
251
|
-
logger.info("Migration shim methods: #{printer_hash.pp_s}.") unless printer_hash.empty?
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
# @return the class => attributes hash for terminal path attributes which can be customized by +migrate_+ methods
|
256
|
-
def customizable_class_attributes
|
257
|
-
# The customizable classes set, starting with creatable classes and adding in
|
258
|
-
# the migration path terminal attribute declarer classes below.
|
259
|
-
klasses = @creatable_classes.to_set
|
260
|
-
# the class => path terminal attributes hash
|
261
|
-
cls_attrs_hash = LazyHash.new { Set.new }
|
262
|
-
# add each path terminal attribute and its declarer class
|
263
|
-
@cls_paths_hash.each_value do |paths|
|
264
|
-
paths.each do |path|
|
265
|
-
attr_md = path.last
|
266
|
-
type = attr_md.declarer
|
267
|
-
klasses << type
|
268
|
-
cls_attrs_hash[type] << attr_md
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
# Merge each redundant customizable superclass into its concrete customizable subclasses.
|
273
|
-
klasses.dup.each do |cls|
|
274
|
-
redundant = false
|
275
|
-
klasses.each do |other|
|
276
|
-
# cls is redundant if it is a superclass of other
|
277
|
-
redundant = other < cls
|
278
|
-
if redundant then
|
279
|
-
cls_attrs_hash[other].merge!(cls_attrs_hash[cls])
|
280
|
-
end
|
281
|
-
end
|
282
|
-
# remove the redundant class
|
283
|
-
if redundant then
|
284
|
-
cls_attrs_hash.delete(cls)
|
285
|
-
klasses.delete(cls)
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
cls_attrs_hash
|
290
|
-
end
|
291
|
-
|
292
|
-
# Discovers methods of the form +migrate+__attribute_ implemented for the paths
|
293
|
-
# in the given class => paths hash the given klass. The migrate method is called
|
294
|
-
# on the input field value corresponding to the path.
|
295
|
-
def migration_filters(klass, attr_mds)
|
296
|
-
# the attribute => migration method hash
|
297
|
-
mth_hash = attribute_method_hash(klass, attr_mds)
|
298
|
-
proc_hash = attribute_proc_hash(klass, attr_mds)
|
299
|
-
return if mth_hash.empty? and proc_hash.empty?
|
300
|
-
|
301
|
-
# for each class path terminal attribute metadata, add the migration filters
|
302
|
-
# to the attribute metadata => filter hash
|
303
|
-
attr_mds.to_compact_hash do |attr_md|
|
304
|
-
# the filter proc
|
305
|
-
proc = proc_hash[attr_md.to_sym]
|
306
|
-
# the migration shim method
|
307
|
-
mth = mth_hash[attr_md.to_sym]
|
308
|
-
if mth then
|
309
|
-
if proc then
|
310
|
-
Proc.new do |obj, value, row|
|
311
|
-
# filter the value
|
312
|
-
fval = proc.call(value)
|
313
|
-
# call the migration method on the filtered value
|
314
|
-
obj.send(mth, fval, row) unless fval.nil?
|
315
|
-
end
|
316
|
-
else
|
317
|
-
# call the migration method
|
318
|
-
Proc.new { |obj, value, row| obj.send(mth, value, row) }
|
319
|
-
end
|
320
|
-
elsif proc then
|
321
|
-
# call the filter
|
322
|
-
Proc.new { |obj, value, row| proc.call(value) }
|
323
|
-
end
|
324
|
-
end
|
325
|
-
end
|
326
|
-
|
327
|
-
def attribute_method_hash(klass, attr_mds)
|
328
|
-
# the migrate methods, excluding the Migratable migrate_references method
|
329
|
-
mths = klass.instance_methods(true).select { |mth| mth =~ /^migrate.(?!references)/ }
|
330
|
-
# the attribute => migration method hash
|
331
|
-
mth_hash = {}
|
332
|
-
mths.each do |mth|
|
333
|
-
# the attribute suffix, e.g. name for migrate_name or Name for migrateName
|
334
|
-
suffix = /^migrate(_)?(.*)/.match(mth).captures[1]
|
335
|
-
# the attribute name
|
336
|
-
attr_nm = suffix[0, 1].downcase + suffix[1..-1]
|
337
|
-
# the attribute for the name, or skip if no such attribute
|
338
|
-
attr = klass.standard_attribute(attr_nm) rescue next
|
339
|
-
# associate the attribute => method
|
340
|
-
mth_hash[attr] = mth
|
341
|
-
end
|
342
|
-
mth_hash
|
343
|
-
end
|
344
|
-
|
345
|
-
# @return [Attribute => {Object => Object}] the filter migration methods
|
346
|
-
def attribute_proc_hash(klass, attr_mds)
|
347
|
-
hash = @filter_hash[klass]
|
348
|
-
if hash.nil? then return Hash::EMPTY_HASH end
|
349
|
-
proc_hash = {}
|
350
|
-
attr_mds.each do |attr_md|
|
351
|
-
flt = hash[attr_md.to_sym] || next
|
352
|
-
proc_hash[attr_md.to_sym] = to_filter_proc(flt)
|
353
|
-
end
|
354
|
-
logger.debug { "Migration filters loaded for #{klass.qp} #{proc_hash.keys.to_series}." }
|
355
|
-
proc_hash
|
356
|
-
end
|
357
|
-
|
358
|
-
# Builds a proc that filters the input value. The config filter mapping entry is one of the following:
|
359
|
-
# * literal: literal
|
360
|
-
# * regexp: literal
|
361
|
-
# * regexp: template
|
362
|
-
#
|
363
|
-
# The regexp template can include match references (+$1+, +$2+, etc.) corresponding to the regexp captures.
|
364
|
-
# If the input value equals a literal, then the mapped literal is returned. Otherwise, if the input value
|
365
|
-
# matches a regexp, then the mapped transformation is returned after reference substitution. Otherwise,
|
366
|
-
# the input value is returned unchanged.
|
367
|
-
#
|
368
|
-
# For example, the config:
|
369
|
-
# /(\d{1,2})\/x\/(\d{1,2})/: $1/1/$2
|
370
|
-
# n/a: ~
|
371
|
-
# converts the input value as follows:
|
372
|
-
# 3/12/02 => 3/12/02 (no match)
|
373
|
-
# 5/x/04 => 5/1/04
|
374
|
-
# n/a => nil
|
375
|
-
#
|
376
|
-
# @param [{Object => Object}] filter the config value mapping
|
377
|
-
# @return [Proc] the filter migration block
|
378
|
-
# @raise [MigrationError] if the filter includes a regexp option other than +i+ (case-insensitive)
|
379
|
-
def to_filter_proc(filter)
|
380
|
-
# Split the filter into a straight value => value hash and a pattern => value hash.
|
381
|
-
ph, vh = filter.split { |k, v| k =~ REGEXP_PAT }
|
382
|
-
# The Regexp => value hash is built from the pattern => value hash.
|
383
|
-
reh = {}
|
384
|
-
ph.each do |k, v|
|
385
|
-
# The /pattern/opts string is parsed to the pattern and options.
|
386
|
-
pat, opt = REGEXP_PAT.match(k).captures
|
387
|
-
# Convert the regexp i option character to a Regexp initializer parameter.
|
388
|
-
reopt = if opt then
|
389
|
-
case opt
|
390
|
-
when 'i' then Regexp::IGNORECASE
|
391
|
-
else raise MigrationError.new("Migration value filter regular expression #{k} qualifier not supported: expected 'i', found '#{opt}'")
|
392
|
-
end
|
393
|
-
end
|
394
|
-
# the Regexp object
|
395
|
-
re = Regexp.new(pat, reopt)
|
396
|
-
# The regexp value can include match references ($1, $2, etc.). In that case, replace the $
|
397
|
-
# match reference with a %s print reference, since the filter formats the matching input value.
|
398
|
-
reh[re] = String === v ? v.gsub(/\$\d/, '%s') : v
|
399
|
-
end
|
400
|
-
# The new proc matches preferentially on the literal value, then the first matching regexp.
|
401
|
-
# If no match on either a literal or a regexp, then the value is preserved.
|
402
|
-
Proc.new do |value|
|
403
|
-
if vh.has_key?(value) then
|
404
|
-
vh[value]
|
405
|
-
else
|
406
|
-
# The first regex which matches the value.
|
407
|
-
regexp = reh.detect_key { |re| value =~ re }
|
408
|
-
# If there is a match, then apply the filter to the match data.
|
409
|
-
# Otherwise, pass the value through unmodified.
|
410
|
-
if regexp then
|
411
|
-
v = reh[regexp]
|
412
|
-
String === v ? v % $~.captures : v
|
413
|
-
else
|
414
|
-
value
|
415
|
-
end
|
416
|
-
end
|
417
|
-
end
|
418
|
-
end
|
419
|
-
|
420
|
-
# Loads the shim files.
|
421
|
-
#
|
422
|
-
# @param [<String>, String] files the file or file array
|
423
|
-
def load_shims(files)
|
424
|
-
logger.debug { "Loading shims with load path #{$:.pp_s}..." }
|
425
|
-
files.enumerate do |file|
|
426
|
-
# load the file
|
427
|
-
begin
|
428
|
-
require file
|
429
|
-
rescue Exception => e
|
430
|
-
logger.error("Migrator couldn't load shim file #{file} - #{e}.")
|
431
|
-
raise
|
432
|
-
end
|
433
|
-
logger.info { "Migrator loaded shim file #{file}." }
|
434
|
-
end
|
435
|
-
end
|
436
|
-
|
437
|
-
# Migrates all rows in the input.
|
438
|
-
#
|
439
|
-
# @yield (see #migrate)
|
440
|
-
# @yieldparam (see #migrate)
|
441
|
-
def migrate_rows
|
442
|
-
# open an CSV output for bad records if the option is set
|
443
|
-
if @bad_rec_file then
|
444
|
-
@loader.trash = @bad_rec_file
|
445
|
-
logger.info("Unmigrated records will be written to #{File.expand_path(@bad_rec_file)}.")
|
446
|
-
end
|
447
|
-
@rec_cnt = mgt_cnt = 0
|
448
|
-
logger.info { "Migrating #{@input}..." }
|
449
|
-
@loader.each do |row|
|
450
|
-
# the one-based current record number
|
451
|
-
rec_no = @rec_cnt + 1
|
452
|
-
# skip if the row precedes the offset option
|
453
|
-
@rec_cnt += 1 && next if @rec_cnt < @offset
|
454
|
-
begin
|
455
|
-
# migrate the row
|
456
|
-
logger.debug { "Migrating record #{rec_no}..." }
|
457
|
-
target = migrate_row(row)
|
458
|
-
# call the block on the migrated target
|
459
|
-
if target then
|
460
|
-
logger.debug { "Migrator built #{target} with the following content:\n#{target.dump}" }
|
461
|
-
yield target
|
462
|
-
end
|
463
|
-
rescue Exception => e
|
464
|
-
trace = e.backtrace.join("\n")
|
465
|
-
logger.error("Migration error on record #{rec_no} - #{e.message}:\n#{trace}")
|
466
|
-
raise unless @bad_file
|
467
|
-
end
|
468
|
-
if target then
|
469
|
-
# replace the log message below with the commented alternative to detect a memory leak
|
470
|
-
logger.debug { "Migrated record #{rec_no}." }
|
471
|
-
#memory_usage = `ps -o rss= -p #{Process.pid}`.to_f / 1024 # in megabytes
|
472
|
-
#logger.debug { "Migrated rec #{@rec_cnt}; memory usage: #{sprintf("%.1f", memory_usage)} MB." }
|
473
|
-
if @print_progress then print_progress(mgt_cnt) end
|
474
|
-
mgt_cnt += 1
|
475
|
-
# clear the migration state
|
476
|
-
clear(target)
|
477
|
-
else
|
478
|
-
# If there is a bad file then warn, reject and continue. Otherwise, bail.
|
479
|
-
if @bad_rec_file then
|
480
|
-
logger.warn("Migration not performed on record #{rec_no}.")
|
481
|
-
@loader.reject(row)
|
482
|
-
else
|
483
|
-
raise MigrationError.new("Migration not performed on record #{rec_no}")
|
484
|
-
end
|
485
|
-
end
|
486
|
-
# Bump the record count.
|
487
|
-
@rec_cnt += 1
|
488
|
-
end
|
489
|
-
logger.info("Migrated #{mgt_cnt} of #{@rec_cnt} records.")
|
490
|
-
end
|
491
|
-
|
492
|
-
# Prints a +\++ progress indicator to stdout if the count parameter is divisible by ten.
|
493
|
-
#
|
494
|
-
# @param [Integer] count the progress step count
|
495
|
-
def print_progress(count)
|
496
|
-
if count % 720 then puts end
|
497
|
-
if count % 10 == 0 then puts "+" else print "+" end
|
498
|
-
end
|
499
|
-
|
500
|
-
# Clears references to objects allocated for migration of a single row into the given target.
|
501
|
-
# This method does nothing. Subclasses can override.
|
502
|
-
#
|
503
|
-
# This method is overridden by subclasses to clear the migration state to conserve memory,
|
504
|
-
# since this migrator should consume O(1) rather than O(n) memory for n migration records.
|
505
|
-
def clear(target)
|
506
|
-
end
|
507
|
-
|
508
|
-
# Imports the given CSV row into a target object.
|
509
|
-
#
|
510
|
-
# @param [{Symbol => Object}] row the input row field => value hash
|
511
|
-
# @return the migrated target object if the migration is valid, nil otherwise
|
512
|
-
def migrate_row(row)
|
513
|
-
# create an instance for each creatable class
|
514
|
-
created = Set.new
|
515
|
-
migrated = @creatable_classes.map { |klass| create(klass, row, created) }
|
516
|
-
# migrate each object from the input row
|
517
|
-
created.each { |obj| obj.migrate(row, migrated) }
|
518
|
-
valid = migrate_valid_references(row, migrated)
|
519
|
-
# the target object
|
520
|
-
target = valid.detect { |obj| @target_class === obj } || return
|
521
|
-
logger.debug { "Migrated target #{target}." }
|
522
|
-
target
|
523
|
-
end
|
524
|
-
|
525
|
-
# Sets the migration references for each valid migrated object.
|
526
|
-
#
|
527
|
-
# @param [Array] the migrated objects
|
528
|
-
# @return [Array] the valid migrated objects
|
529
|
-
def migrate_valid_references(row, migrated)
|
530
|
-
# Split the valid and invalid objects. The iteration is in reverse dependency order,
|
531
|
-
# since invalidating a dependent can invalidate the owner.
|
532
|
-
valid, invalid = migrated.transitive_closure(:dependents).reverse.partition do |obj|
|
533
|
-
if migration_valid?(obj) then
|
534
|
-
obj.migrate_references(row, migrated, @attr_flt_hash[obj.class])
|
535
|
-
true
|
536
|
-
else
|
537
|
-
obj.class.owner_attributes.each { |attr| obj.clear_attribute(attr) }
|
538
|
-
false
|
539
|
-
end
|
540
|
-
end
|
541
|
-
|
542
|
-
# Go back through the valid objects in dependency order to invalidate dependents
|
543
|
-
# whose owner is invalid.
|
544
|
-
valid.reverse.each do |obj|
|
545
|
-
unless owner_valid?(obj, valid, invalid) then
|
546
|
-
invalid << valid.delete(obj)
|
547
|
-
logger.debug { "Invalidated migrated #{obj} since it does not have a valid owner." }
|
548
|
-
end
|
549
|
-
end
|
550
|
-
|
551
|
-
# Go back through the valid objects in reverse dependency order to invalidate owners
|
552
|
-
# created only to hold a dependent which was subsequently invalidated.
|
553
|
-
valid.reject do |obj|
|
554
|
-
if @owners.include?(obj.class) and obj.dependents.all? { |dep| invalid.include?(dep) } then
|
555
|
-
# clear all references from the invalidated owner
|
556
|
-
obj.class.domain_attributes.each_metadata { |attr_md| obj.clear_attribute(attr_md.to_sym) }
|
557
|
-
invalid << obj
|
558
|
-
logger.debug { "Invalidated #{obj.qp} since it was created solely to hold subsequently invalidated dependents." }
|
559
|
-
true
|
560
|
-
end
|
561
|
-
end
|
562
|
-
end
|
563
|
-
|
564
|
-
# Returns whether the given domain object satisfies at least one of the following conditions:
|
565
|
-
# * it does not have an owner among the invalid objects
|
566
|
-
# * it has an owner among the valid objects
|
567
|
-
#
|
568
|
-
# @param [Resource] obj the domain object to check
|
569
|
-
# @param [<Resource>] valid the valid migrated objects
|
570
|
-
# @param [<Resource>] invalid the invalid migrated objects
|
571
|
-
# @return [Boolean] whether the owner is valid
|
572
|
-
def owner_valid?(obj, valid, invalid)
|
573
|
-
otypes = obj.class.owners
|
574
|
-
invalid.all? { |other| not otypes.include?(other.class) } or
|
575
|
-
valid.any? { |other| otypes.include?(other.class) }
|
576
|
-
end
|
577
|
-
|
578
|
-
# @param [Migratable] obj the migrated object
|
579
|
-
# @return [Boolean] whether the migration is successful
|
580
|
-
def migration_valid?(obj)
|
581
|
-
if obj.migration_valid? then
|
582
|
-
true
|
583
|
-
else
|
584
|
-
logger.debug { "Migrated #{obj.qp} is invalid." }
|
585
|
-
false
|
586
|
-
end
|
587
|
-
end
|
588
|
-
|
589
|
-
# Creates an instance of the given klass from the given row.
|
590
|
-
# The new klass instance and all intermediate migrated instances are added to the
|
591
|
-
# created set.
|
592
|
-
#
|
593
|
-
# @param [Class] klass
|
594
|
-
# @param [{Symbol => Object}] row the input row
|
595
|
-
# @param [<Resource>] created the migrated instances for this row
|
596
|
-
# @return [Resource] the new instance
|
597
|
-
def create(klass, row, created)
|
598
|
-
# the new object
|
599
|
-
logger.debug { "Migrator building #{klass.qp}..." }
|
600
|
-
created << obj = klass.new
|
601
|
-
migrate_attributes(obj, row, created)
|
602
|
-
add_defaults(obj, row, created)
|
603
|
-
logger.debug { "Migrator built #{obj}." }
|
604
|
-
obj
|
605
|
-
end
|
606
|
-
|
607
|
-
# @param [Resource] the migration object
|
608
|
-
# @param row (see #create)
|
609
|
-
# @param [<Resource>] created (see #create)
|
610
|
-
def migrate_attributes(obj, row, created)
|
611
|
-
# for each input header which maps to a migratable target attribute metadata path,
|
612
|
-
# set the target attribute, creating intermediate objects as needed.
|
613
|
-
@cls_paths_hash[obj.class].each do |path|
|
614
|
-
header = @header_map[path][obj.class]
|
615
|
-
# the input value
|
616
|
-
value = row[header]
|
617
|
-
next if value.nil?
|
618
|
-
# fill the reference path
|
619
|
-
ref = fill_path(obj, path[0...-1], row, created)
|
620
|
-
# set the attribute
|
621
|
-
migrate_attribute(ref, path.last, value, row)
|
622
|
-
end
|
623
|
-
end
|
624
|
-
|
625
|
-
# @param [Resource] the migration object
|
626
|
-
# @param row (see #create)
|
627
|
-
# @param [<Resource>] created (see #create)
|
628
|
-
def add_defaults(obj, row, created)
|
629
|
-
dh = @def_hash[obj.class] || return
|
630
|
-
dh.each do |path, value|
|
631
|
-
# fill the reference path
|
632
|
-
ref = fill_path(obj, path[0...-1], row, created)
|
633
|
-
# set the attribute to the default value unless there is already a value
|
634
|
-
ref.merge_attribute(path.last.to_sym, value)
|
635
|
-
end
|
636
|
-
end
|
637
|
-
|
638
|
-
# Fills the given reference Attribute path starting at obj.
|
639
|
-
#
|
640
|
-
# @param row (see #create)
|
641
|
-
# @param created (see #create)
|
642
|
-
# @return the last domain object in the path
|
643
|
-
def fill_path(obj, path, row, created)
|
644
|
-
# create the intermediate objects as needed (or return obj if path is empty)
|
645
|
-
path.inject(obj) do |parent, attr_md|
|
646
|
-
# the referenced object
|
647
|
-
parent.send(attr_md.reader) or create_reference(parent, attr_md, row, created)
|
648
|
-
end
|
649
|
-
end
|
650
|
-
|
651
|
-
# Sets the given migrated object's reference attribute to a new referenced domain object.
|
652
|
-
#
|
653
|
-
# @param [Resource] obj the domain object being migrated
|
654
|
-
# @param [Attribute] attr_md the attribute being migrated
|
655
|
-
# @param row (see #create)
|
656
|
-
# @param created (see #create)
|
657
|
-
# @return the new object
|
658
|
-
def create_reference(obj, attr_md, row, created)
|
659
|
-
if attr_md.type.abstract? then
|
660
|
-
raise MigrationError.new("Cannot create #{obj.qp} #{attr_md} with abstract type #{attr_md.type}")
|
661
|
-
end
|
662
|
-
ref = attr_md.type.new
|
663
|
-
ref.migrate(row, Array::EMPTY_ARRAY)
|
664
|
-
obj.send(attr_md.writer, ref)
|
665
|
-
created << ref
|
666
|
-
logger.debug { "Migrator created #{obj.qp} #{attr_md} #{ref}." }
|
667
|
-
ref
|
668
|
-
end
|
669
|
-
|
670
|
-
# Sets the given attribute value to the filtered input value. If there is a filter
|
671
|
-
# defined for the attribute, then that filter is applied. If there is a migration
|
672
|
-
# shim method with name +migrate_+_attribute_, then than method is called on the
|
673
|
-
# (possibly filtered) value. The target object attribute is set to the resulting
|
674
|
-
# filtered value.
|
675
|
-
#
|
676
|
-
# @param [Migratable] obj the target domain object
|
677
|
-
# @param [Attribute] attr_md the target attribute
|
678
|
-
# @param value the input value
|
679
|
-
# @param [{Symbol => Object}] row the input row
|
680
|
-
def migrate_attribute(obj, attr_md, value, row)
|
681
|
-
# if there is a shim migrate_<attribute> method, then call it on the input value
|
682
|
-
value = filter_value(obj, attr_md, value, row) || return
|
683
|
-
# set the attribute
|
684
|
-
begin
|
685
|
-
obj.send(attr_md.writer, value)
|
686
|
-
rescue Exception
|
687
|
-
raise MigrationError.new("Could not set #{obj.qp} #{attr_md} to #{value.qp} - #{$!}")
|
688
|
-
end
|
689
|
-
logger.debug { "Migrated #{obj.qp} #{attr_md} to #{value}." }
|
690
|
-
end
|
691
|
-
|
692
|
-
# Calls the shim migrate_<attribute> method or config filter on the input value.
|
693
|
-
#
|
694
|
-
# @param value the input value
|
695
|
-
# @return the input value, if there is no filter, otherwise the filtered value
|
696
|
-
def filter_value(obj, attr_md, value, row)
|
697
|
-
filter = filter_for(obj, attr_md)
|
698
|
-
return value if filter.nil?
|
699
|
-
fval = filter.call(obj, value, row)
|
700
|
-
unless value == fval then
|
701
|
-
logger.debug { "Migration filter transformed #{obj.qp} #{attr_md} value from #{value.qp} to #{fval.qp}." }
|
702
|
-
end
|
703
|
-
fval
|
704
|
-
end
|
705
|
-
|
706
|
-
def filter_for(obj, attr_md)
|
707
|
-
flts = @attr_flt_hash[obj.class] || return
|
708
|
-
flts[attr_md]
|
709
|
-
end
|
710
|
-
|
711
|
-
# @param [Resource] obj the domain object to save in the database
|
712
|
-
# @return [Resource, nil] obj if the save is successful, nil otherwise
|
713
|
-
def save(obj, database)
|
714
|
-
if @create then
|
715
|
-
if database.find(obj) then
|
716
|
-
logger.debug { "Migrator ignored record #{current_record}, since it already exists as #{obj.printable_content(obj.class.secondary_key_attributes)} with id #{obj.identifier}." }
|
717
|
-
else
|
718
|
-
logger.debug { "Migrator creating #{obj}..." }
|
719
|
-
database.create(obj)
|
720
|
-
logger.debug { "Migrator creating #{obj}." }
|
721
|
-
end
|
722
|
-
else
|
723
|
-
logger.debug { "Migrator saving #{obj}..." }
|
724
|
-
database.save(obj)
|
725
|
-
logger.debug { "Migrator saved #{obj}." }
|
726
|
-
end
|
727
|
-
end
|
728
|
-
|
729
|
-
def current_record
|
730
|
-
@rec_cnt + 1
|
731
|
-
end
|
732
|
-
|
733
|
-
# @param [<String>, String] files the migration fields mapping file or file array
|
734
|
-
# @return [{Class => {Attribute => Symbol}}] the class => path => header hash
|
735
|
-
# loaded from the mapping files
|
736
|
-
def load_field_map_files(files)
|
737
|
-
map = LazyHash.new { Hash.new }
|
738
|
-
files.enumerate { |file| load_field_map_file(file, map) }
|
739
|
-
|
740
|
-
# include the target class
|
741
|
-
map[@target_class] ||= Hash.new
|
742
|
-
# include the default classes
|
743
|
-
@def_hash.each_key { |klass| map[klass] ||= Hash.new }
|
744
|
-
|
745
|
-
# add superclass paths into subclass paths
|
746
|
-
map.each do |klass, path_hdr_hash|
|
747
|
-
map.each do |other, other_path_hdr_hash|
|
748
|
-
if klass < other then
|
749
|
-
# add, but don't replace, path => header entries from superclass
|
750
|
-
path_hdr_hash.merge!(other_path_hdr_hash) { |key, old, new| old }
|
751
|
-
end
|
752
|
-
end
|
753
|
-
end
|
754
|
-
|
755
|
-
# include only concrete classes
|
756
|
-
classes = map.keys
|
757
|
-
map.delete_if do |klass, paths|
|
758
|
-
klass.abstract? or classes.any? { |other| other < klass }
|
759
|
-
end
|
760
|
-
|
761
|
-
map
|
762
|
-
end
|
763
|
-
|
764
|
-
# @param [String] file the migration fields configuration file
|
765
|
-
# @param [{Class => {Attribute => Symbol}}] hash the class => path => header hash
|
766
|
-
# loaded from the configuration file
|
767
|
-
def load_field_map_file(file, hash)
|
768
|
-
# load the field mapping config file
|
769
|
-
begin
|
770
|
-
config = YAML::load_file(file)
|
771
|
-
rescue
|
772
|
-
raise MigrationError.new("Could not read field map file #{file}: " + $!)
|
773
|
-
end
|
774
|
-
|
775
|
-
# collect the class => path => header entries
|
776
|
-
config.each do |field, attr_list|
|
777
|
-
next if attr_list.blank?
|
778
|
-
# the header accessor method for the field
|
779
|
-
header = @loader.accessor(field)
|
780
|
-
raise MigrationError.new("Field defined in migration configuration #{file} not found in input file #{@input} headers: #{field}") if header.nil?
|
781
|
-
# associate each attribute path in the property value with the header
|
782
|
-
attr_list.split(/,\s*/).each do |path_s|
|
783
|
-
klass, path = create_attribute_path(path_s)
|
784
|
-
hash[klass][path] = header
|
785
|
-
end
|
786
|
-
end
|
787
|
-
end
|
788
|
-
|
789
|
-
# Loads the defaults config files.
|
790
|
-
#
|
791
|
-
# @param [<String>, String] files the file or file array to load
|
792
|
-
# @return [<Class => <String => Object>>] the class => path => default value entries
|
793
|
-
def load_defaults_files(files)
|
794
|
-
# collect the class => path => value entries from each defaults file
|
795
|
-
hash = LazyHash.new { Hash.new }
|
796
|
-
files.enumerate { |file| load_defaults_file(file, hash) }
|
797
|
-
hash
|
798
|
-
end
|
799
|
-
|
800
|
-
# Loads the defaults config file into the given hash.
|
801
|
-
#
|
802
|
-
# @param [String] file the file to load
|
803
|
-
# @param [<Class => <String => Object>>] hash the class => path => default value entries
|
804
|
-
def load_defaults_file(file, hash)
|
805
|
-
begin
|
806
|
-
config = YAML::load_file(file)
|
807
|
-
rescue
|
808
|
-
raise MigrationError.new("Could not read defaults file #{file}: " + $!)
|
809
|
-
end
|
810
|
-
# collect the class => path => value entries
|
811
|
-
config.each do |path_s, value|
|
812
|
-
next if value.nil_or_empty?
|
813
|
-
klass, path = create_attribute_path(path_s)
|
814
|
-
hash[klass][path] = value
|
815
|
-
end
|
816
|
-
end
|
817
|
-
# Loads the filter config files.
|
818
|
-
#
|
819
|
-
# @param [<String>, String] files the file or file array to load
|
820
|
-
# @return [<Class => <String => Object>>] the class => path => default value entries
|
821
|
-
def load_filter_files(files)
|
822
|
-
# collect the class => path => value entries from each defaults file
|
823
|
-
hash = {}
|
824
|
-
files.enumerate { |file| load_filter_file(file, hash) }
|
825
|
-
hash
|
826
|
-
end
|
827
|
-
|
828
|
-
# Loads the filter config file into the given hash.
|
829
|
-
#
|
830
|
-
# @param [String] file the file to load
|
831
|
-
# @param [<Class => <String => <Object => Object>>>] hash the class => path => input value => caTissue value entries
|
832
|
-
def load_filter_file(file, hash)
|
833
|
-
begin
|
834
|
-
config = YAML::load_file(file)
|
835
|
-
rescue
|
836
|
-
raise MigrationError.new("Could not read filter file #{file}: " + $!)
|
837
|
-
end
|
838
|
-
# collect the class => attribute => filter entries
|
839
|
-
config.each do |path_s, flt|
|
840
|
-
next if flt.nil_or_empty?
|
841
|
-
klass, path = create_attribute_path(path_s)
|
842
|
-
unless path.size == 1 then
|
843
|
-
raise MigrationError.new("Migration filter configuration path not supported: #{path_s}")
|
844
|
-
end
|
845
|
-
attr = klass.standard_attribute(path.first.to_sym)
|
846
|
-
flt_hash = hash[klass] ||= {}
|
847
|
-
flt_hash[attr] = flt
|
848
|
-
end
|
849
|
-
end
|
850
|
-
|
851
|
-
# @param [String] path_s a period-delimited path string path_s in the form _class_(._attribute_)+
|
852
|
-
# @return [<Attribute>] the corresponding attribute metadata path
|
853
|
-
# @raise [MigrationError] if the path string is malformed or an attribute is not found
|
854
|
-
def create_attribute_path(path_s)
|
855
|
-
names = path_s.split('.')
|
856
|
-
# if the path starts with a capitalized class name, then resolve the class.
|
857
|
-
# otherwise, the target class is the start of the path.
|
858
|
-
klass = names.first =~ /^[A-Z]/ ? class_for_name(names.shift) : @target_class
|
859
|
-
# there must be at least one attribute
|
860
|
-
if names.empty? then
|
861
|
-
raise MigrationError.new("Attribute entry in migration configuration is not in <class>.<attribute> format: #{value}")
|
862
|
-
end
|
863
|
-
# build the Attribute path
|
864
|
-
path = []
|
865
|
-
names.inject(klass) do |parent, name|
|
866
|
-
attr = name.to_sym
|
867
|
-
attr_md = begin
|
868
|
-
parent.attribute_metadata(attr)
|
869
|
-
rescue NameError
|
870
|
-
raise MigrationError.new("Migration field mapping attribute #{parent.qp}.#{attr} not found: #{$!}")
|
871
|
-
end
|
872
|
-
if attr_md.collection? then
|
873
|
-
raise MigrationError.new("Migration field mapping attribute #{parent.qp}.#{attr} is a collection, which is not supported")
|
874
|
-
end
|
875
|
-
path << attr_md
|
876
|
-
attr_md.type
|
877
|
-
end
|
878
|
-
# return the starting class and Attribute path.
|
879
|
-
# note that the starting class is not necessarily the first path attribute declarer, since the
|
880
|
-
# starting class could be the concrete target class rather than an abstract declarer. this is
|
881
|
-
# important, since the class must be instantiated.
|
882
|
-
[klass, path]
|
883
|
-
end
|
884
|
-
|
885
|
-
# @param [String] name the class name, without the {#context_module}
|
886
|
-
# @return [Class] the corresponding class
|
887
|
-
def class_for_name(name)
|
888
|
-
# navigate through the scope to the final class
|
889
|
-
name.split('::').inject(context_module) do |ctxt, cnm|
|
890
|
-
ctxt.const_get(cnm)
|
891
|
-
end
|
892
|
-
end
|
893
|
-
|
894
|
-
# The context module is given by the target class {ResourceClass#domain_module}.
|
895
|
-
#
|
896
|
-
# @return [Module] the class name resolution context
|
897
|
-
def context_module
|
898
|
-
@target_class.domain_module
|
899
|
-
end
|
900
|
-
|
901
|
-
# @return a new class => [paths] hash from the migration fields configuration map
|
902
|
-
def create_class_paths_hash(fld_map, def_map)
|
903
|
-
hash = {}
|
904
|
-
fld_map.each { |klass, path_hdr_hash| hash[klass] = path_hdr_hash.keys.to_set }
|
905
|
-
def_map.each { |klass, path_val_hash| (hash[klass] ||= Set.new).merge(path_val_hash.keys) }
|
906
|
-
hash
|
907
|
-
end
|
908
|
-
|
909
|
-
# @return a new path => class => header hash from the migration fields configuration map
|
910
|
-
def create_header_map(fld_map)
|
911
|
-
hash = LazyHash.new { Hash.new }
|
912
|
-
fld_map.each do |klass, path_hdr_hash|
|
913
|
-
path_hdr_hash.each { |path, hdr| hash[path][klass] = hdr }
|
914
|
-
end
|
915
|
-
hash
|
916
|
-
end
|
917
|
-
end
|
918
|
-
end
|