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,338 @@
|
|
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/inflector'
|
12
|
+
require 'caruby/util/collection'
|
13
|
+
|
14
|
+
# include some standard Java classes
|
15
|
+
module Java
|
16
|
+
|
17
|
+
# Adds the directories in the given path and all Java jar files contained in the directories
|
18
|
+
# to the execution classpath.
|
19
|
+
#
|
20
|
+
# @param path the colon or semi-colon separated directories
|
21
|
+
def self.add_path(path)
|
22
|
+
# the path directories
|
23
|
+
dirs = path.split(/[:;]/).map { |dir| File.expand_path(dir) }
|
24
|
+
# Add all jars found anywhere within the directories to the the classpath.
|
25
|
+
add_jars(*dirs)
|
26
|
+
# Add the directories to the the classpath.
|
27
|
+
dirs.each { |dir| $CLASSPATH << dir }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds the jars in the directories to the execution class path.
|
31
|
+
#
|
32
|
+
# @param directories the directories containing jars to add
|
33
|
+
def self.add_jars(*directories)
|
34
|
+
directories.each do |dir|
|
35
|
+
Dir[File.join(dir , "**", "*.jar")].each { |jar| $CLASSPATH << jar }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module JavaUtil
|
40
|
+
# Aliases Java Collection methods with the standard Ruby Set counterpart, e.g. +delete+ for +remove+.
|
41
|
+
module Collection
|
42
|
+
def to_a
|
43
|
+
inject(Array.new) { |array, item| array << item }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Removes the given item from this collection.
|
47
|
+
def delete(item)
|
48
|
+
# can't alias delete to remove, since a Java interface doesn't implement any methods
|
49
|
+
remove(item)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Removes the items from this collection for which the block given to this method returns a non-nil, non-false value.
|
53
|
+
def delete_if
|
54
|
+
removeAll(select { |item| yield item })
|
55
|
+
self
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Aliases Java List methods with the standard Ruby Array counterpart, e.g. +merge+ for +addAll+.
|
60
|
+
module List
|
61
|
+
# Returns whether this List has the same content as the other Java List or Ruby Array.
|
62
|
+
def ==(other)
|
63
|
+
Array === other ? to_a == other : equals(other)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Removes the given item from this collection.
|
67
|
+
def delete(item)
|
68
|
+
remove(item)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module Map
|
73
|
+
# Returns whether this Set has the same content as the other Java Map or Ruby Hash.
|
74
|
+
def ==(other)
|
75
|
+
::Hash === other ? (size == other.size and other.all? { |key, value| get(key) == value }) : equals(other)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Merges the other Java Map or Ruby Hash into this Map. Returns this modified Map.
|
79
|
+
#
|
80
|
+
# If a block is given to this method, then the block determines the mapped value
|
81
|
+
# as specified in the Ruby Hash merge method documentation.
|
82
|
+
def merge(other)
|
83
|
+
other.each do |key, value|
|
84
|
+
value = yield(key, get(key), value) if block_given? and containsKey(key)
|
85
|
+
put(key, value)
|
86
|
+
end
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
alias :merge! :merge
|
91
|
+
end
|
92
|
+
|
93
|
+
module Set
|
94
|
+
# Returns whether this Set has the same content as the other Java Set or Ruby Set.
|
95
|
+
def ==(other)
|
96
|
+
::Set === other ? (size == other.size and all? { |item| other.include?(item) }) : equals(other)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Merges the other Enumerable into this Set. Returns this modified Set.
|
100
|
+
#
|
101
|
+
# This method conforms to the Ruby Set merge contract rather than the Ruby List and Hash
|
102
|
+
# merge contract. Ruby Set merge modifies the Set in-place, whereas Ruby List and Hash
|
103
|
+
# merge return a new collection.
|
104
|
+
def merge(other)
|
105
|
+
return self if other.nil?
|
106
|
+
raise ArgumentError.new("Merge argument must be enumerable: #{other}") unless Enumerable === other
|
107
|
+
other.each { |item| self << item }
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
alias :merge! :merge
|
112
|
+
end
|
113
|
+
|
114
|
+
class HashSet
|
115
|
+
alias :base__clear :clear
|
116
|
+
private :base__clear
|
117
|
+
def clear
|
118
|
+
base__clear
|
119
|
+
self
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class TreeSet
|
124
|
+
alias :base__first :first
|
125
|
+
private :base__first
|
126
|
+
# Fixes the jRuby {TreeSet#first} to return nil on an empty set rather than raise a Java exception.
|
127
|
+
def first
|
128
|
+
empty? ? nil : base__first
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class ArrayList
|
133
|
+
alias :base__clear :clear
|
134
|
+
private :base__clear
|
135
|
+
def clear
|
136
|
+
base__clear
|
137
|
+
self
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class Date
|
142
|
+
# millisecond-to-day conversion factor
|
143
|
+
MILLIS_PER_HR = 60 * 60 * 1000
|
144
|
+
MILLIS_PER_DAY = MILLIS_PER_HR * 24
|
145
|
+
|
146
|
+
# Converts this Java Date to a Ruby DateTime.
|
147
|
+
#
|
148
|
+
# caTissue alert - Bug #165: API CPR create date validation is time zone dependent.
|
149
|
+
# Since Java Date accounts for DST and Ruby DateTime doesn't,
|
150
|
+
# this method makes the DST adjustment by subtracting a compensatory
|
151
|
+
# one-hour DST offset from the Java Date time zone offset and using
|
152
|
+
# that to set the DateTime offset. This ensures that Date
|
153
|
+
# conversion is idempotent, i.e.
|
154
|
+
# date.to_ruby_date().to_java_date == date
|
155
|
+
#
|
156
|
+
# However, there can be adverse consequences for an application that assumes
|
157
|
+
# that the client time zone is the same as the server time zone, as described
|
158
|
+
# in caTissue Bug #165.
|
159
|
+
#
|
160
|
+
# TODO: Revisit {CaRuby::Resource.value_equal?} which must resort to a
|
161
|
+
# date-as-string comparison, always a bad idea. If that can be fixed, then
|
162
|
+
# increment/decrement the hour field rather than the offset field.
|
163
|
+
#
|
164
|
+
# @return [DateTime] the Ruby date
|
165
|
+
def to_ruby_date
|
166
|
+
calendar = java.util.Calendar.instance
|
167
|
+
calendar.setTime(self)
|
168
|
+
secs = calendar.timeInMillis / 1000
|
169
|
+
# millis since epoch
|
170
|
+
time = Time.at(secs)
|
171
|
+
# convert UTC timezone millisecond offset to Rational fraction of a day
|
172
|
+
offset_millis = calendar.timeZone.getOffset(calendar.timeInMillis).to_f
|
173
|
+
# adjust for DST
|
174
|
+
if calendar.timeZone.useDaylightTime and not time.isdst then
|
175
|
+
offset_millis -= MILLIS_PER_HR
|
176
|
+
end
|
177
|
+
offset_days = offset_millis / MILLIS_PER_DAY
|
178
|
+
offset_fraction = 1 / offset_days
|
179
|
+
offset = Rational(1, offset_fraction)
|
180
|
+
# convert to DateTime
|
181
|
+
DateTime.civil(time.year, time.mon, time.day, time.hour, time.min, time.sec, offset)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Converts a Ruby Date or DateTime to a Java Date.
|
185
|
+
#
|
186
|
+
# @param [::Date, DateTime] date the Ruby date
|
187
|
+
# @return [Date] the Java date
|
188
|
+
def self.from_ruby_date(date)
|
189
|
+
return if date.nil?
|
190
|
+
# DateTime has time attributes, Date doesn't
|
191
|
+
if DateTime === date then
|
192
|
+
hour, min, sec = date.hour, date.min, date.sec
|
193
|
+
else
|
194
|
+
hour = min = sec = 0
|
195
|
+
end
|
196
|
+
# the Ruby time
|
197
|
+
time = Time.mktime(date.year, date.mon, date.day, hour, min, sec)
|
198
|
+
# millis since epoch
|
199
|
+
millis = (time.to_f * 1000).truncate
|
200
|
+
# the Java date factory
|
201
|
+
calendar = java.util.Calendar.instance
|
202
|
+
# adjust for DST
|
203
|
+
if calendar.timeZone.useDaylightTime and not Time.at(time).isdst then
|
204
|
+
millis += MILLIS_PER_HR
|
205
|
+
end
|
206
|
+
calendar.setTimeInMillis(millis)
|
207
|
+
calendar.getTime
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.now
|
213
|
+
JavaUtil::Date.from_ruby_date(DateTime.now)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the Java package name for the full class_name, or nil if
|
217
|
+
# class_name is unqualified.
|
218
|
+
def self.java_package_name(class_name)
|
219
|
+
prefix = class_name[/(\w+\.)+/]
|
220
|
+
# remove the trailing period
|
221
|
+
prefix.chop! if prefix
|
222
|
+
prefix
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
class Class
|
227
|
+
# Returns whether this is a Java wrapper class.
|
228
|
+
def java_class?
|
229
|
+
method_defined?(:java_class)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Returns a Ruby class for the given klass. If klass is already a Ruby Class, then returns klass.
|
233
|
+
# If klass is a String, then returns the Ruby wrapper class for the corresponding Java class name.
|
234
|
+
# Otherwise, this method returns the Ruby class for the name of the presumed Java klass.
|
235
|
+
def self.to_ruby(klass)
|
236
|
+
case klass
|
237
|
+
when Class then klass
|
238
|
+
when String then Java.module_eval(klass)
|
239
|
+
else to_ruby(klass.name)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns whether this class is abstract.
|
244
|
+
def abstract?
|
245
|
+
java_class? and Java::JavaLangReflect::Modifier.isAbstract(java_class.modifiers)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Returns whether the given PropertyDescriptor pd corresponds to a transient field in this class, or nil if there is no such field.
|
249
|
+
def transient?(pd)
|
250
|
+
begin
|
251
|
+
field = java_class.declared_field(pd.name)
|
252
|
+
rescue Exception
|
253
|
+
# should occur only if a property is not a field; not an error
|
254
|
+
return
|
255
|
+
end
|
256
|
+
Java::JavaLangReflect::Modifier.isTransient(field.modifiers) if field
|
257
|
+
end
|
258
|
+
|
259
|
+
# Returns this class's readable and writable Java PropertyDescriptors, or an empty Array if none.
|
260
|
+
# If the hierarchy flag is set to +false+, then only this class's properties
|
261
|
+
# will be introspected.
|
262
|
+
def java_properties(hierarchy=true)
|
263
|
+
info = hierarchy ? Java::JavaBeans::Introspector.getBeanInfo(java_class) : Java::JavaBeans::Introspector.getBeanInfo(java_class, java_class.superclass)
|
264
|
+
info.propertyDescriptors.select { |pd| pd.write_method and property_read_method(pd) }
|
265
|
+
end
|
266
|
+
|
267
|
+
# Redefines the reserved method corresponeding to the given Java property descriptor pd
|
268
|
+
# back to the Object implementation, if necessary.
|
269
|
+
# If both this class and Object define a method with the property name,
|
270
|
+
# then a new method is defined with the same body as the previous method.
|
271
|
+
# Returns the new method symbol, or nil if name_or_symbol is not an occluded
|
272
|
+
# Object instance method.
|
273
|
+
#
|
274
|
+
# This method undoes the jRuby clobbering of Object methods by Java property method
|
275
|
+
# wrappers. The method is renamed as follows:
|
276
|
+
# * +id+ is changed to :identifier
|
277
|
+
# * +type+ is prefixed by the underscore subject class name, e.g. +Specimen.type => :specimen_type+,
|
278
|
+
# If the property name is +type+ and the subject class name ends in 'Type', then the attribute
|
279
|
+
# symbol is the underscore subject class name, e.g. +HistologicType.type => :histologic_type+.
|
280
|
+
#
|
281
|
+
# Raises ArgumentError if symbol is not an Object method.
|
282
|
+
def unocclude_reserved_method(pd)
|
283
|
+
oldname = pd.name.underscore
|
284
|
+
return unless OBJ_INST_MTHDS.include?(oldname)
|
285
|
+
oldsym = oldname.to_sym
|
286
|
+
undeprecated = case oldsym
|
287
|
+
when :id then :object_id
|
288
|
+
when :type then :class
|
289
|
+
else oldsym
|
290
|
+
end
|
291
|
+
rsvd_mth = Object.instance_method(undeprecated)
|
292
|
+
base = self.qp.underscore
|
293
|
+
newname = if oldname == 'id' then
|
294
|
+
'identifier'
|
295
|
+
elsif base[-oldname.length..-1] == oldname then
|
296
|
+
base
|
297
|
+
else
|
298
|
+
"#{base}_#{oldname}"
|
299
|
+
end
|
300
|
+
newsym = newname.to_sym
|
301
|
+
rdr = property_read_method(pd).name.to_sym
|
302
|
+
alias_method(newsym, rdr)
|
303
|
+
# alias the writers
|
304
|
+
wtr = pd.write_method.name.to_sym
|
305
|
+
alias_method("#{newsym}=".to_sym, wtr)
|
306
|
+
# alias a camel-case Java-style method if necessary
|
307
|
+
altname = newname.camelize
|
308
|
+
unless altname == newname then
|
309
|
+
alias_method(altname.to_sym, oldsym)
|
310
|
+
alias_method("#{altname}=".to_sym, wtr)
|
311
|
+
end
|
312
|
+
# restore the old method to Object
|
313
|
+
define_method(oldsym) { |*args| rsvd_mth.bind(self).call(*args) }
|
314
|
+
newsym
|
315
|
+
end
|
316
|
+
|
317
|
+
# Returns the property descriptor pd introspected or discovered Java read Method.
|
318
|
+
def property_read_method(pd)
|
319
|
+
return pd.read_method if pd.read_method
|
320
|
+
# caCORE alert - java.lang.Boolean is<name> is not introspected as a read method, since type must be primitive boolean is<name>
|
321
|
+
return unless pd.get_property_type == Java::JavaLang::Boolean.java_class
|
322
|
+
rdr = java_class.java_method("is#{pd.name.capitalize_first}") rescue nil
|
323
|
+
logger.debug { "Discovered #{qp} #{pd.name} property non-introspected reader method #{rdr.name}." } if rdr
|
324
|
+
rdr
|
325
|
+
end
|
326
|
+
|
327
|
+
private
|
328
|
+
|
329
|
+
OBJ_INST_MTHDS = Object.instance_methods
|
330
|
+
end
|
331
|
+
|
332
|
+
class Array
|
333
|
+
alias :equal__base :==
|
334
|
+
# Overrides the standard == to compare a Java List with a Ruby Array.
|
335
|
+
def ==(other)
|
336
|
+
Java::JavaUtil::List === other ? other == self : equal__base(other)
|
337
|
+
end
|
338
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'caruby/migration/resource_module'
|
2
|
+
|
3
|
+
module CaRuby
|
4
|
+
# A Migratable mix-in adds migration support for Resource domain objects.
|
5
|
+
# For each migration Resource created by a CaRuby::Migrator, the migration process
|
6
|
+
# is as follows:
|
7
|
+
#
|
8
|
+
# 1. The migrator creates the Resource using the empty constructor.
|
9
|
+
#
|
10
|
+
# 2. Each input field value which maps to a Resource attribute is obtained from the
|
11
|
+
# migration source.
|
12
|
+
#
|
13
|
+
# 3. If the Resource class implements a method +migrate_+_attribute_ for the
|
14
|
+
# migration _attribute_, then that migrate method is called with the input value
|
15
|
+
# argument. If there is a migrate method, then the attribute is set to the
|
16
|
+
# result of calling that method, otherwise the attribute is set to the original
|
17
|
+
# input value.
|
18
|
+
#
|
19
|
+
# For example, if the +Name+ input field maps to +Participant.name+, then a
|
20
|
+
# custom +Participant+ +migrate_name+ shim method can be defined to reformat
|
21
|
+
# the input name.
|
22
|
+
#
|
23
|
+
# 4. The Resource attribute is set to the (possibly modified) value.
|
24
|
+
#
|
25
|
+
# 5. After all input fields are processed, then {#migration_valid?} is called to
|
26
|
+
# determine whether the migrated object can be used. {#migration_valid?} is true
|
27
|
+
# by default, but a migration shim can add a validation check,
|
28
|
+
# migrated Resource class to return false for special cases.
|
29
|
+
#
|
30
|
+
# For example, a custom +Participant+ +migration_valid?+ shim method can be
|
31
|
+
# defined to return whether there is a non-empty input field value.
|
32
|
+
#
|
33
|
+
# 6. After the migrated objects are validated, then the Migrator fills in
|
34
|
+
# dependency hierarchy gaps. For example, if the Resource class +Participant+
|
35
|
+
# owns the +enrollments+ dependent which in turn owns the +encounters+ dependent
|
36
|
+
# and the migration has created a +Participant+ and an +Encounter+ but no +Enrollment+,
|
37
|
+
# then an empty +Enrollment+ is created which is owned by the migrated +Participant+
|
38
|
+
# and owns the migrated +Encounter+.
|
39
|
+
#
|
40
|
+
# 7. After all dependencies are filled in, then the independent references are set
|
41
|
+
# for each created Resource (including the new dependents). If a created
|
42
|
+
# Resource has an independent non-collection Resource reference attribute
|
43
|
+
# and there is a migrated instance of that attribute type, then the attribute
|
44
|
+
# is set to that migrated instance.
|
45
|
+
#
|
46
|
+
# For example, if +Enrollment+ has a +study+ attribute and there is a
|
47
|
+
# single migrated +Study+ instance, then the +study+ attribute is set
|
48
|
+
# to that migrated +Study+ instance.
|
49
|
+
#
|
50
|
+
# If the referencing class implements a method +migrate_+_attribute_ for the
|
51
|
+
# migration _attribute_, then that migrate method is called with the referenced
|
52
|
+
# instance argument. The result is used to set the attribute. Otherwise, the
|
53
|
+
# attribute is set to the original referenced instance.
|
54
|
+
#
|
55
|
+
# There must be a single unambiguous candidate independent instance, e.g. in the
|
56
|
+
# unlikely but conceivable case that two +Study+ instances are migrated, then the
|
57
|
+
# +study+ attribute is not set. Similarly, collection attributes are not set,
|
58
|
+
# e.g. a +Study+ +protocols+ attribute is not set to a migrated +Protocol+
|
59
|
+
# instance.
|
60
|
+
#
|
61
|
+
# 8. The {#migrate} method is called to complete the migration. As described in the
|
62
|
+
# method documentation, a migration shim Resource subclass can override the
|
63
|
+
# method for custom migration processing, e.g. to migrate the ambiguous or
|
64
|
+
# collection attributes mentioned above, or to fill in missing values.
|
65
|
+
#
|
66
|
+
# Note that there is an extensive set of attribute defaults defined in
|
67
|
+
# the CaRuby::ResourceMetadata application domain classes. These defaults
|
68
|
+
# are applied in a migration database save action and need not be set in
|
69
|
+
# a migration shim. For example, if an acceptable default for a +Study+
|
70
|
+
# +active?+ flag is defined in the +Study+ meta-data, then the flag does not
|
71
|
+
# need to be set in a migration shim.
|
72
|
+
module Migratable
|
73
|
+
# Completes setting this Migratable domain object's attributes from the given input row.
|
74
|
+
# This method is responsible for migrating attributes which are not mapped
|
75
|
+
# in the configuration. It is called after the configuration attributes for
|
76
|
+
# the given row are migrated and before {#migrate_references}.
|
77
|
+
#
|
78
|
+
# This base implementation is a no-op.
|
79
|
+
# Subclasses can modify this method to complete the migration. The overridden
|
80
|
+
# methods should call +super+ to pick up the superclass migration.
|
81
|
+
#
|
82
|
+
# @param [Hash] row the input row
|
83
|
+
# @param [Enumerable] migrated the migrated instances, including this Resource
|
84
|
+
def migrate(row, migrated)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns whether this migration target domain object is valid. The default is true
|
88
|
+
# if this domain object either has no owner or its owner is valid.
|
89
|
+
# A migration shim should override this method on the target if there are conditions
|
90
|
+
# which determine whether the migration should be skipped for this target object.
|
91
|
+
#
|
92
|
+
# @return [Boolean] whether this migration target domain object is valid
|
93
|
+
def migration_valid?
|
94
|
+
# check that the owner is be valid
|
95
|
+
ownr = owner
|
96
|
+
ownr.nil? or ownr.migration_valid?
|
97
|
+
end
|
98
|
+
|
99
|
+
# Migrates this domain object's migratable references. This method is called by the
|
100
|
+
# CaRuby::Migrator and should not be overridden by subclasses. Subclasses tailor
|
101
|
+
# individual reference attribute migration by defining a +migrate_+_attribute_ method
|
102
|
+
# for the _attribute_ to modify.
|
103
|
+
#
|
104
|
+
# The migratable reference attributes consist of the non-collection
|
105
|
+
# {ResourceAttributes#saved_independent_attributes} which don't already have a value.
|
106
|
+
# For each such migratable attribute, if there is a single instance of the attribute
|
107
|
+
# type in the given migrated domain objects, then the attribute is set to that
|
108
|
+
# migrated instance.
|
109
|
+
#
|
110
|
+
# If the attribute is associated with a method in mth_hash, then that method is called
|
111
|
+
# on the migrated instance and input row. The attribute is set to the method return value.
|
112
|
+
# mth_hash includes an entry for each +migrate_+_attribute_ method defined by this
|
113
|
+
# Resource's class.
|
114
|
+
#
|
115
|
+
# @param [{Symbol => Object}] row the input row field => value hash
|
116
|
+
# @param [<Resource>] migrated the migrated instances, including this Resource
|
117
|
+
# @param [{Symbol => String}] mth_hash a hash that associates this domain object's
|
118
|
+
# attributes to migration method names
|
119
|
+
def migrate_references(row, migrated, mth_hash=nil)
|
120
|
+
self.class.saved_independent_attributes.each do |attr|
|
121
|
+
ref = migratable__reference_value(attr, migrated)
|
122
|
+
migratable__set_reference(attr, ref, row, mth_hash) if ref
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# @param [Symbol] attribute the reference attribute to get
|
129
|
+
# @param migrated (see #migrate_references)
|
130
|
+
# @return [Resource, nil] the migrated value to which the attribute will be set
|
131
|
+
def migratable__reference_value(attribute, migrated)
|
132
|
+
# skip non-nil attributes
|
133
|
+
return if send(attribute)
|
134
|
+
# the attribute metadata, used for type information
|
135
|
+
attr_md = self.class.attribute_metadata(attribute)
|
136
|
+
# skip collection attributes
|
137
|
+
return if attr_md.collection?
|
138
|
+
# the migrated references which are instances of the attribute type
|
139
|
+
refs = migrated.select { |other| other != self and attr_md.type === other }
|
140
|
+
# skip ambiguous references
|
141
|
+
return unless refs.size == 1
|
142
|
+
# the single reference
|
143
|
+
ref = refs.first
|
144
|
+
end
|
145
|
+
|
146
|
+
# Sets the given migrated domain object attribute to the given reference.
|
147
|
+
#
|
148
|
+
# If the attribute is associated to a method in mth_hash, then that method is called on
|
149
|
+
# the migrated instance and input row. The attribute is set to the method return value.
|
150
|
+
# mth_hash includes an entry for each +migrate_+_attribute_ method defined by this
|
151
|
+
# Resource's class.
|
152
|
+
#
|
153
|
+
# @param [Symbol] (see #migratable__reference_value)
|
154
|
+
# @param [Resource] ref the migrated reference
|
155
|
+
# @param row (see #migrate_references)
|
156
|
+
# @param mth_hash (see #migrate_references)
|
157
|
+
def migratable__set_reference(attribute, ref, row, mth_hash=nil)
|
158
|
+
# the shim method, if any
|
159
|
+
mth = mth_hash[attribute] if mth_hash
|
160
|
+
# if there is a shim method, then call it
|
161
|
+
ref = send(mth, ref, row) if mth and respond_to?(mth)
|
162
|
+
return if ref.nil?
|
163
|
+
logger.debug { "Setting #{qp} #{attribute} to migrated #{ref.qp}..." }
|
164
|
+
set_attribute(attribute, ref)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|