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,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
|