caruby-core 1.4.2 → 1.4.3
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 +10 -0
- data/lib/caruby/cli/command.rb +10 -8
- data/lib/caruby/database/fetched_matcher.rb +28 -39
- data/lib/caruby/database/lazy_loader.rb +101 -0
- data/lib/caruby/database/persistable.rb +190 -167
- data/lib/caruby/database/persistence_service.rb +21 -7
- data/lib/caruby/database/persistifier.rb +185 -0
- data/lib/caruby/database/reader.rb +106 -176
- data/lib/caruby/database/saved_matcher.rb +56 -0
- data/lib/caruby/database/search_template_builder.rb +1 -1
- data/lib/caruby/database/sql_executor.rb +8 -7
- data/lib/caruby/database/store_template_builder.rb +134 -61
- data/lib/caruby/database/writer.rb +252 -52
- data/lib/caruby/database.rb +88 -67
- data/lib/caruby/domain/attribute_initializer.rb +16 -0
- data/lib/caruby/domain/attribute_metadata.rb +161 -72
- data/lib/caruby/domain/id_alias.rb +22 -0
- data/lib/caruby/domain/inversible.rb +91 -0
- data/lib/caruby/domain/merge.rb +116 -35
- data/lib/caruby/domain/properties.rb +1 -1
- data/lib/caruby/domain/reference_visitor.rb +207 -71
- data/lib/caruby/domain/resource_attributes.rb +93 -80
- data/lib/caruby/domain/resource_dependency.rb +22 -97
- data/lib/caruby/domain/resource_introspection.rb +21 -28
- data/lib/caruby/domain/resource_inverse.rb +134 -0
- data/lib/caruby/domain/resource_metadata.rb +41 -19
- data/lib/caruby/domain/resource_module.rb +42 -33
- data/lib/caruby/import/java.rb +8 -9
- data/lib/caruby/migration/migrator.rb +20 -7
- data/lib/caruby/migration/resource_module.rb +0 -2
- data/lib/caruby/resource.rb +132 -351
- data/lib/caruby/util/cache.rb +4 -1
- data/lib/caruby/util/class.rb +48 -1
- data/lib/caruby/util/collection.rb +54 -18
- data/lib/caruby/util/inflector.rb +7 -0
- data/lib/caruby/util/options.rb +35 -31
- data/lib/caruby/util/partial_order.rb +1 -1
- data/lib/caruby/util/properties.rb +2 -2
- data/lib/caruby/util/stopwatch.rb +16 -8
- data/lib/caruby/util/transitive_closure.rb +1 -1
- data/lib/caruby/util/visitor.rb +342 -328
- data/lib/caruby/version.rb +1 -1
- data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
- data/lib/caruby.rb +2 -0
- metadata +10 -9
- data/lib/caruby/database/saved_merger.rb +0 -131
- data/lib/caruby/domain/annotatable.rb +0 -25
- data/lib/caruby/domain/annotation.rb +0 -23
- data/lib/caruby/import/annotatable_class.rb +0 -28
- data/lib/caruby/import/annotation_class.rb +0 -27
- data/lib/caruby/import/annotation_module.rb +0 -67
- data/lib/caruby/migration/resource.rb +0 -8
data/lib/caruby/import/java.rb
CHANGED
@@ -11,9 +11,7 @@ require 'caruby/util/class'
|
|
11
11
|
require 'caruby/util/inflector'
|
12
12
|
require 'caruby/util/collection'
|
13
13
|
|
14
|
-
# include some standard Java classes
|
15
14
|
module Java
|
16
|
-
|
17
15
|
# Adds the directories in the given path and all Java jar files contained in the directories
|
18
16
|
# to the execution classpath.
|
19
17
|
#
|
@@ -165,13 +163,13 @@ module Java
|
|
165
163
|
def to_ruby_date
|
166
164
|
calendar = java.util.Calendar.instance
|
167
165
|
calendar.setTime(self)
|
168
|
-
secs = calendar.timeInMillis / 1000
|
166
|
+
secs = calendar.timeInMillis.to_f / 1000
|
169
167
|
# millis since epoch
|
170
168
|
time = Time.at(secs)
|
171
169
|
# convert UTC timezone millisecond offset to Rational fraction of a day
|
172
170
|
offset_millis = calendar.timeZone.getOffset(calendar.timeInMillis).to_f
|
173
171
|
# adjust for DST
|
174
|
-
if calendar.timeZone.useDaylightTime and
|
172
|
+
if calendar.timeZone.useDaylightTime and time.isdst then
|
175
173
|
offset_millis -= MILLIS_PER_HR
|
176
174
|
end
|
177
175
|
offset_days = offset_millis / MILLIS_PER_DAY
|
@@ -200,7 +198,7 @@ module Java
|
|
200
198
|
# the Java date factory
|
201
199
|
calendar = java.util.Calendar.instance
|
202
200
|
# adjust for DST
|
203
|
-
if calendar.timeZone.useDaylightTime and
|
201
|
+
if calendar.timeZone.useDaylightTime and Time.at(time).isdst then
|
204
202
|
millis += MILLIS_PER_HR
|
205
203
|
end
|
206
204
|
calendar.setTimeInMillis(millis)
|
@@ -234,13 +232,13 @@ class Class
|
|
234
232
|
# Otherwise, this method returns the Ruby class for the name of the presumed Java klass.
|
235
233
|
def self.to_ruby(klass)
|
236
234
|
case klass
|
237
|
-
|
238
|
-
|
239
|
-
|
235
|
+
when Class then klass
|
236
|
+
when String then Java.module_eval(klass)
|
237
|
+
else to_ruby(klass.name)
|
240
238
|
end
|
241
239
|
end
|
242
240
|
|
243
|
-
#
|
241
|
+
# @return [Boolean] whether this is a wrapper for an abstract Java class
|
244
242
|
def abstract?
|
245
243
|
java_class? and Java::JavaLangReflect::Modifier.isAbstract(java_class.modifiers)
|
246
244
|
end
|
@@ -260,6 +258,7 @@ class Class
|
|
260
258
|
# If the hierarchy flag is set to +false+, then only this class's properties
|
261
259
|
# will be introspected.
|
262
260
|
def java_properties(hierarchy=true)
|
261
|
+
return Array::EMPTY_ARRAY unless java_class?
|
263
262
|
info = hierarchy ? Java::JavaBeans::Introspector.getBeanInfo(java_class) : Java::JavaBeans::Introspector.getBeanInfo(java_class, java_class.superclass)
|
264
263
|
info.propertyDescriptors.select { |pd| pd.write_method and property_read_method(pd) }
|
265
264
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# load the required gems
|
2
2
|
require 'rubygems'
|
3
3
|
|
4
|
-
# the
|
4
|
+
# the Units of Measurement gem
|
5
5
|
gem 'uom'
|
6
6
|
|
7
7
|
require 'enumerator'
|
@@ -15,7 +15,7 @@ require 'caruby/util/options'
|
|
15
15
|
require 'caruby/util/pretty_print'
|
16
16
|
require 'caruby/util/properties'
|
17
17
|
require 'caruby/util/collection'
|
18
|
-
require 'caruby/migration/
|
18
|
+
require 'caruby/migration/migratable'
|
19
19
|
|
20
20
|
module CaRuby
|
21
21
|
class MigrationError < RuntimeError; end
|
@@ -33,7 +33,8 @@ module CaRuby
|
|
33
33
|
# @option opts [String] :input required source file to migrate
|
34
34
|
# @option opts [String] :shims optional array of shim files to load
|
35
35
|
# @option opts [String] :bad optional invalid record file
|
36
|
-
# @option opts [
|
36
|
+
# @option opts [Integer] :offset zero-based starting source record number to process (default 0)
|
37
|
+
# @option opts [Boolean] :quiet suppress output messages
|
37
38
|
def initialize(opts)
|
38
39
|
parse_options(opts)
|
39
40
|
build
|
@@ -72,6 +73,7 @@ module CaRuby
|
|
72
73
|
migrate do |target|
|
73
74
|
save(target, db)
|
74
75
|
yield target if block_given?
|
76
|
+
db.clear
|
75
77
|
end
|
76
78
|
end
|
77
79
|
end
|
@@ -97,12 +99,14 @@ module CaRuby
|
|
97
99
|
@offset = opts[:offset] ||= 0
|
98
100
|
@input = Options.get(:input, opts)
|
99
101
|
raise MigrationError.new("Migrator missing required source file parameter") if @input.nil?
|
100
|
-
@database =
|
102
|
+
@database = opts[:database]
|
101
103
|
raise MigrationError.new("Migrator missing required database parameter") if @database.nil?
|
102
|
-
@target_class =
|
104
|
+
@target_class = opts[:target]
|
103
105
|
raise MigrationError.new("Migrator missing required target class parameter") if @target_class.nil?
|
104
106
|
@bad_rec_file = opts[:bad]
|
105
107
|
logger.info("Migration options: #{opts.reject { |option, value| value.nil_or_empty? }.pp_s}.")
|
108
|
+
# flag indicating whether to print a progress monitor
|
109
|
+
@print_progress = !opts[:quiet]
|
106
110
|
end
|
107
111
|
|
108
112
|
def build
|
@@ -282,6 +286,7 @@ module CaRuby
|
|
282
286
|
rec_cnt += 1 && next if rec_cnt < @offset
|
283
287
|
begin
|
284
288
|
# migrate the row
|
289
|
+
logger.debug { "Migrating record #{rec_no}..." }
|
285
290
|
target = migrate_row(row)
|
286
291
|
# call the block on the migrated target
|
287
292
|
if target then
|
@@ -298,6 +303,7 @@ module CaRuby
|
|
298
303
|
logger.debug { "Migrated record #{rec_no}." }
|
299
304
|
#memory_usage = `ps -o rss= -p #{Process.pid}`.to_f / 1024 # in megabytes
|
300
305
|
#logger.debug { "Migrated rec #{@rec_cnt}; memory usage: #{sprintf("%.1f", memory_usage)} MB." }
|
306
|
+
if @print_progress then print_progress(mgt_cnt) end
|
301
307
|
mgt_cnt += 1
|
302
308
|
# clear the migration state
|
303
309
|
clear(target)
|
@@ -315,6 +321,13 @@ module CaRuby
|
|
315
321
|
end
|
316
322
|
logger.info("Migrated #{mgt_cnt} of #{rec_cnt} records.")
|
317
323
|
end
|
324
|
+
|
325
|
+
# Prints a +\++ progress indicator to stdout.
|
326
|
+
#
|
327
|
+
# @param [Integer] count the progress step count
|
328
|
+
def print_progress(count)
|
329
|
+
if count % 72 == 0 then puts "+" else print "+" end
|
330
|
+
end
|
318
331
|
|
319
332
|
# Clears references to objects allocated for migration of a single row into the given target.
|
320
333
|
# This method does nothing. Subclasses can override.
|
@@ -334,10 +347,10 @@ module CaRuby
|
|
334
347
|
migrated = @creatable_classes.map { |klass| create(klass, row, created) }
|
335
348
|
# migrate each object from the input row
|
336
349
|
created.each { |obj| obj.migrate(row, migrated) }
|
337
|
-
# set the references
|
338
|
-
migrated.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
|
339
350
|
# remove invalid migrations
|
340
351
|
migrated.delete_if { |obj| not migration_valid?(obj) }
|
352
|
+
# set the references
|
353
|
+
migrated.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
|
341
354
|
# the target object
|
342
355
|
target = migrated.detect { |obj| @target_class === obj }
|
343
356
|
if target then
|
data/lib/caruby/resource.rb
CHANGED
@@ -7,40 +7,17 @@ require 'caruby/util/collection'
|
|
7
7
|
require 'caruby/domain/merge'
|
8
8
|
require 'caruby/domain/reference_visitor'
|
9
9
|
require 'caruby/database/persistable'
|
10
|
+
require 'caruby/domain/inversible'
|
10
11
|
require 'caruby/domain/resource_metadata'
|
11
12
|
require 'caruby/domain/resource_module'
|
13
|
+
require 'caruby/migration/migratable'
|
12
14
|
|
13
15
|
module CaRuby
|
14
16
|
# The Domain module is included by Java domain classes.
|
15
17
|
# This module defines essential common domain methods that enable the jRuby-Java API bridge.
|
16
18
|
# Classes which include Domain must implement the +metadata+ Domain::Metadata accessor method.
|
17
19
|
module Resource
|
18
|
-
include
|
19
|
-
|
20
|
-
# @param [{Symbol => Object}] the optional attribute => value hash
|
21
|
-
# @return a new instance of this Resource class initialized from the optional attribute => value hash
|
22
|
-
def initialize(hash=nil)
|
23
|
-
super()
|
24
|
-
if hash then
|
25
|
-
unless Hashable === hash then
|
26
|
-
raise ArgumentError.new("#{qp} initializer argument type not supported: #{hash.class.qp}")
|
27
|
-
end
|
28
|
-
merge_attributes(hash)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# Returns the database identifier.
|
33
|
-
# This method aliases a Java +id+ property accessor, since +id+ is a reserved Ruby attribute.
|
34
|
-
def identifier
|
35
|
-
getId
|
36
|
-
end
|
37
|
-
|
38
|
-
# Sets the database identifier to the given id value.
|
39
|
-
# This method aliases the caTissue Java +id+ property setter,
|
40
|
-
# since +id+ is a reserved Ruby attribute.
|
41
|
-
def identifier=(id)
|
42
|
-
setId(id)
|
43
|
-
end
|
20
|
+
include Mergeable, Migratable, Persistable, Inversible, Validation
|
44
21
|
|
45
22
|
# Sets the default attribute values for this domain object and its dependents. If this Resource
|
46
23
|
# does not have an identifier, then missing attributes are set to the values defined by
|
@@ -52,7 +29,7 @@ module CaRuby
|
|
52
29
|
# @return [Resource] self
|
53
30
|
def add_defaults
|
54
31
|
# apply owner defaults
|
55
|
-
if owner then
|
32
|
+
if owner and owner.identifier.nil? then
|
56
33
|
owner.add_defaults
|
57
34
|
else
|
58
35
|
logger.debug { "Adding defaults to #{qp} and its dependents..." }
|
@@ -71,7 +48,7 @@ module CaRuby
|
|
71
48
|
# @raise [ValidationError] if a mandatory attribute value is missing
|
72
49
|
def validate
|
73
50
|
unless @validated then
|
74
|
-
logger.debug { "Validating #{qp} required attributes #{self.
|
51
|
+
logger.debug { "Validating #{qp} required attributes #{self.mandatory_attributes.to_a.to_series}..." }
|
75
52
|
invalid = missing_mandatory_attributes
|
76
53
|
unless invalid.empty? then
|
77
54
|
logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
|
@@ -124,61 +101,10 @@ module CaRuby
|
|
124
101
|
self.class.new.merge_attributes(self, attributes)
|
125
102
|
end
|
126
103
|
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
# @param [
|
131
|
-
# @yield [source, target] mergethe source into the target
|
132
|
-
# @yieldparam [Resource] source the reference to merge from
|
133
|
-
# @yieldparam [Resource] target the reference the source is merge into
|
134
|
-
# @raise [ArgumentError] if other is not an instance of this domain object's class
|
135
|
-
# @see #merge_attribute_value
|
136
|
-
def merge_match(other, attributes, &matcher)
|
137
|
-
raise ArgumentError.new("Incompatible #{qp} merge source: #{other.qp}") unless self.class === other
|
138
|
-
logger.debug { format_merge_log_message(other, attributes) }
|
139
|
-
# merge the non-domain attributes
|
140
|
-
merge_attributes(other)
|
141
|
-
# merge the domain attributes
|
142
|
-
unless attributes.nil_or_empty? then
|
143
|
-
merge_attributes(other, attributes) do |attr, oldval, newval|
|
144
|
-
merge_attribute_value(attr, oldval, newval, &matcher)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
# Merges the attribute newval into oldval as follows:
|
150
|
-
# * If attribute is a non-domain attribute and oldval is either nil or the attribute is not saved,
|
151
|
-
# then the attribute value is set to newval.
|
152
|
-
# * Otherwise, if attribute is a domain non-collection attribute, then newval is recursively
|
153
|
-
# merged into oldval.
|
154
|
-
# * Otherwise, if attribute is a domain collection attribute, then matching newval members are
|
155
|
-
# merged into the corresponding oldval member and non-matching newval members are added to
|
156
|
-
# newval.
|
157
|
-
# * Otherwise, the attribute value is not changed.
|
158
|
-
#
|
159
|
-
# The domain value match is performed by the matcher block. The block arguments are newval and
|
160
|
-
# oldval and the return value is a source => target hash of matching references. The default
|
161
|
-
# matcher is {Resource#match_in}.
|
162
|
-
#
|
163
|
-
# @param [Symbol] attribute the merge attribute
|
164
|
-
# @param oldval the current value
|
165
|
-
# @param newval the value to merge
|
166
|
-
# @yield [newval, oldval] the value matcher used if attribute is a domain collection
|
167
|
-
# @yieldparam newval the merge source value
|
168
|
-
# @yieldparam oldval this domain object's current attribute value
|
169
|
-
# @return the merged attribute value
|
170
|
-
def merge_attribute_value(attribute, oldval, newval, &matcher)
|
171
|
-
attr_md = self.class.attribute_metadata(attribute)
|
172
|
-
if attr_md.nondomain? then
|
173
|
-
if oldval.nil? and not newval.nil? then
|
174
|
-
send(attr_md.writer, newval)
|
175
|
-
newval
|
176
|
-
end
|
177
|
-
else
|
178
|
-
merge_domain_attribute_value(attr_md, oldval, newval, &matcher)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
104
|
+
# Clears the given attribute value. If the current value responds to the +clear+ method,
|
105
|
+
# then the current value is cleared. Otherwise, the value is set to {ResourceMetadata#empty_value}.
|
106
|
+
#
|
107
|
+
# @param [Symbol] attribute the attribute to clear
|
182
108
|
def clear_attribute(attribute)
|
183
109
|
# the current value to clear
|
184
110
|
current = send(attribute)
|
@@ -188,7 +114,9 @@ module CaRuby
|
|
188
114
|
if current.respond_to?(:clear) then
|
189
115
|
current.clear
|
190
116
|
else
|
191
|
-
|
117
|
+
writer = self.class.attribute_metadata(attribute).writer
|
118
|
+
value = self.class.empty_value(attribute)
|
119
|
+
send(writer, value)
|
192
120
|
end
|
193
121
|
end
|
194
122
|
|
@@ -197,7 +125,7 @@ module CaRuby
|
|
197
125
|
# is preserved, e.g. an Array value is assigned to a set domain type by first clearing the set
|
198
126
|
# and then merging the array content into the set.
|
199
127
|
#
|
200
|
-
# @see #merge_attribute
|
128
|
+
# @see Mergeable#merge_attribute
|
201
129
|
def set_attribute(attribute, value)
|
202
130
|
# bail out if the value argument is the current value
|
203
131
|
return value if value.equal?(send(attribute))
|
@@ -207,26 +135,42 @@ module CaRuby
|
|
207
135
|
|
208
136
|
# Returns the secondary key attribute values as follows:
|
209
137
|
# * If there is no secondary key, then this method returns nil.
|
210
|
-
# * Otherwise, if the secondary key attributes is a singleton Array, then the key is the
|
138
|
+
# * Otherwise, if the secondary key attributes is a singleton Array, then the key is the
|
139
|
+
# value of the sole key attribute.
|
211
140
|
# * Otherwise, the key is an Array of the key attribute values.
|
141
|
+
#
|
142
|
+
# @return [Array, Object] the key attribute values
|
212
143
|
def key
|
213
144
|
attrs = self.class.secondary_key_attributes
|
214
145
|
case attrs.size
|
215
|
-
|
216
|
-
|
217
|
-
|
146
|
+
when 0 then nil
|
147
|
+
when 1 then send(attrs.first)
|
148
|
+
else attrs.map { |attr| send(attr) }
|
218
149
|
end
|
219
150
|
end
|
220
151
|
|
221
|
-
#
|
152
|
+
# @return [Resource, nil] the domain object that owns this object, or nil if this object
|
153
|
+
# is not dependent on an owner
|
222
154
|
def owner
|
223
155
|
self.class.owner_attributes.detect_value { |attr| send(attr) }
|
224
156
|
end
|
157
|
+
|
158
|
+
# Sets this dependent's owner attribute to the given domain object.
|
159
|
+
#
|
160
|
+
# @param [Resource] owner the owner domain object
|
161
|
+
# @raise [NoMethodError] if this Resource's class does not have exactly one owner attribute
|
162
|
+
def owner=(owner)
|
163
|
+
attr = self.class.owner_attribute
|
164
|
+
if attr.nil? then raise NoMethodError.new("#{self.class.qp} does not have a unique owner attribute") end
|
165
|
+
set_attribute(attr, owner)
|
166
|
+
end
|
225
167
|
|
226
|
-
#
|
168
|
+
# @param [Resource] other the domain object to check
|
169
|
+
# @return [Boolean] whether the other domain object is this object's {#owner} or an
|
170
|
+
# {#owner_ancestor?} of this object's {#owner}
|
227
171
|
def owner_ancestor?(other)
|
228
|
-
|
229
|
-
|
172
|
+
owner = self.owner
|
173
|
+
owner and (owner == other or owner.owner_ancestor?(other))
|
230
174
|
end
|
231
175
|
|
232
176
|
# Returns an attribute => value hash for the specified attributes with a non-nil, non-empty value.
|
@@ -249,12 +193,12 @@ module CaRuby
|
|
249
193
|
attributes.map { |attr| send(attr) }.flatten.compact
|
250
194
|
end
|
251
195
|
|
252
|
-
#
|
196
|
+
# @return [Boolean] whether this domain object is dependent on another entity
|
253
197
|
def dependent?
|
254
198
|
self.class.dependent?
|
255
199
|
end
|
256
200
|
|
257
|
-
#
|
201
|
+
# @return [Boolean] whether this domain object is not dependent on another entity
|
258
202
|
def independent?
|
259
203
|
not dependent?
|
260
204
|
end
|
@@ -265,14 +209,23 @@ module CaRuby
|
|
265
209
|
# @yieldparam [Resource] dep the dependent
|
266
210
|
def each_dependent
|
267
211
|
self.class.dependent_attributes.each do |attr|
|
268
|
-
|
212
|
+
send(attr).enumerate { |dep| yield dep }
|
269
213
|
end
|
270
214
|
end
|
271
215
|
|
272
|
-
#
|
216
|
+
# @return [Enumerable] this domain object's dependents
|
273
217
|
def dependents
|
274
218
|
enum_for(:each_dependent)
|
275
219
|
end
|
220
|
+
|
221
|
+
# Returns the attributes which are required for save. This base implementation returns the
|
222
|
+
# class {ResourceAttributes#mandatory_attributes}. Subclasses can override this method
|
223
|
+
# for domain object state-specific refinements.
|
224
|
+
#
|
225
|
+
# @return [<Symbol>] the required attributes for a save operation
|
226
|
+
def mandatory_attributes
|
227
|
+
self.class.mandatory_attributes
|
228
|
+
end
|
276
229
|
|
277
230
|
# Returns the attribute references which directly depend on this owner.
|
278
231
|
# The default is the attribute value.
|
@@ -285,26 +238,21 @@ module CaRuby
|
|
285
238
|
# dependency path, e.g. in caTissue a Specimen is owned by both a SCG and a parent
|
286
239
|
# Specimen. In that case, the SCG direct dependents consist of top-level Specimens
|
287
240
|
# owned by the SCG but not derived from another Specimen.
|
241
|
+
#
|
242
|
+
# @param [Symbol] attribute the dependent attribute
|
243
|
+
# @return [<Resource>] the attribute value, wrapped in an array if necessary
|
288
244
|
def direct_dependents(attribute)
|
289
245
|
deps = send(attribute)
|
290
246
|
case deps
|
291
|
-
|
292
|
-
|
293
|
-
|
247
|
+
when Enumerable then deps
|
248
|
+
when nil then Array::EMPTY_ARRAY
|
249
|
+
else [deps]
|
294
250
|
end
|
295
251
|
end
|
296
252
|
|
297
|
-
#
|
298
|
-
#
|
299
|
-
#
|
300
|
-
# site1.assoc_attribute_values(site2, [:address]) #=> [site1.address, site2.address]
|
301
|
-
def assoc_attribute_values(other, attributes)
|
302
|
-
return {} if other.nil? or attributes.empty?
|
303
|
-
# associate the attribute => value hashes for this object and the other hash into one attribute => [value1, value2] hash
|
304
|
-
value_hash(attributes).assoc_values(other.value_hash(attributes)).values.reject { |values| values.any? { |value| value.nil? } }
|
305
|
-
end
|
306
|
-
|
307
|
-
# Returns whether this object matches the fetched other object on class and key values.
|
253
|
+
# @param [Resource] the domain object to match
|
254
|
+
# @return [Boolean] whether this object matches the fetched other object on class
|
255
|
+
# and key values
|
308
256
|
def match?(other)
|
309
257
|
match_in([other])
|
310
258
|
end
|
@@ -321,6 +269,9 @@ module CaRuby
|
|
321
269
|
# This domain object is matched against the others on the above attributes in succession
|
322
270
|
# until a unique match is found. The key attribute matches are strict, i.e. each
|
323
271
|
# key attribute value must be non-nil and match the other value.
|
272
|
+
#
|
273
|
+
# @param [<Resource>] the candidate domain object matches
|
274
|
+
# @return [Resource, nil] the matching domain object, or nil if no match
|
324
275
|
def match_in(others)
|
325
276
|
# trivial case: self is in others
|
326
277
|
return self if others.include?(self)
|
@@ -332,37 +283,20 @@ module CaRuby
|
|
332
283
|
match_unique_object_with_attributes(others, self.class.alternate_key_attributes)
|
333
284
|
end
|
334
285
|
|
335
|
-
# Returns the match of this domain object in the scope of a matching owner
|
336
|
-
# If
|
337
|
-
#
|
338
|
-
#
|
286
|
+
# Returns the match of this domain object in the scope of a matching owner as follows:
|
287
|
+
# * If {#match_in} returns a match, then that match is the result is used.
|
288
|
+
# * Otherwise, if this is a dependent attribute then the match is attempted on a
|
289
|
+
# secondary key without owner attributes. Defaults are added to this object in order
|
290
|
+
# to pick up potential secondary key values.
|
339
291
|
#
|
340
|
-
# @param
|
341
|
-
# @return
|
292
|
+
# @param (see #match_in)
|
293
|
+
# @return (see #match_in)
|
342
294
|
def match_in_owner_scope(others)
|
343
|
-
match_in(others) or others.detect { |other| match_without_owner_attribute(other) }
|
295
|
+
match_in(others) or others.detect { |other| match_without_owner_attribute?(other) }
|
344
296
|
end
|
345
297
|
|
346
|
-
#
|
347
|
-
#
|
348
|
-
# potential secondary key values.
|
349
|
-
#
|
350
|
-
# @param [<Resource>] other the domain object to match against
|
351
|
-
# @return [Boolean] whether the other domain object matches this domain object on a
|
352
|
-
# secondary key without owner attributes
|
353
|
-
def match_without_owner_attribute(other)
|
354
|
-
oattrs = self.class.owner_attributes
|
355
|
-
return if oattrs.empty?
|
356
|
-
# add defaults to pick up potential secondary key value
|
357
|
-
add_defaults_local
|
358
|
-
# match on the secondary key
|
359
|
-
self.class.secondary_key_attributes.all? do |attr|
|
360
|
-
oattrs.include?(attr) or matches_attribute_value?(other, attr, send(attr))
|
361
|
-
end
|
362
|
-
end
|
363
|
-
|
364
|
-
# @return [{Resouce => Resource}] a source => target hash of the given sources which match the
|
365
|
-
# targets using the {#match_in} method
|
298
|
+
# @return [{Resouce => Resource}] a source => target hash of the given sources which match
|
299
|
+
# the targets using the {#match_in} method
|
366
300
|
def self.match_all(sources, targets)
|
367
301
|
DEF_MATCHER.match(sources, targets)
|
368
302
|
end
|
@@ -465,6 +399,8 @@ module CaRuby
|
|
465
399
|
yield(ref) and ref.visit_owners(&operator) if ref
|
466
400
|
end
|
467
401
|
|
402
|
+
# @param q the PrettyPrint queue
|
403
|
+
# @return [String] the formatted content of this Resource
|
468
404
|
def pretty_print(q)
|
469
405
|
q.text(qp)
|
470
406
|
content = printable_content
|
@@ -472,14 +408,16 @@ module CaRuby
|
|
472
408
|
end
|
473
409
|
|
474
410
|
# Prints this domain object's content and recursively prints the referenced content.
|
475
|
-
# The optional selector block determines the attributes to print. The default is
|
476
|
-
#
|
411
|
+
# The optional selector block determines the attributes to print. The default is the
|
412
|
+
# {ResourceAttributes#java_attributes}. The database lazy loader is disabled during
|
413
|
+
# the execution of this method. Thus, the printed content reflects the transient
|
414
|
+
# in-memory object graph rather than the persistent content.
|
477
415
|
#
|
478
416
|
# @yield [owner] the owner attribute selector
|
479
417
|
# @yieldparam [Resource] owner the domain object to print
|
480
418
|
# @return [String] the domain object content
|
481
419
|
def dump(&selector)
|
482
|
-
DetailPrinter.new(self, &selector).pp_s
|
420
|
+
database.lazy_loader.disable { DetailPrinter.new(self, &selector).pp_s }
|
483
421
|
end
|
484
422
|
|
485
423
|
# Prints this domain object in the format:
|
@@ -508,27 +446,18 @@ module CaRuby
|
|
508
446
|
# @return [{Symbol => String}] the attribute => content hash
|
509
447
|
def printable_content(attributes=nil, &reference_printer) # :yields: reference
|
510
448
|
attributes ||= printworthy_attributes
|
511
|
-
vh =
|
449
|
+
vh = value_hash(attributes)
|
512
450
|
vh.transform { |value| printable_value(value, &reference_printer) }
|
513
451
|
end
|
514
452
|
|
515
453
|
# Returns whether value equals other modulo the given matches according to the following tests:
|
516
454
|
# * _value_ == _other_
|
517
|
-
# * _value_ and _other_ are Ruby DateTime instances and _value_ equals _other_,
|
518
|
-
# modulo the Java-Ruby DST differences described in the following paragraph
|
519
|
-
# * _value_ == matches[_other_]
|
520
455
|
# * _value_ and _other_ are Resource instances and _value_ is a {#match?} with _other_.
|
521
456
|
# * _value_ and _other_ are Enumerable with members equal according to the above conditions.
|
522
|
-
#
|
523
|
-
# of a date attribute.
|
457
|
+
# * _value_ and _other_ are DateTime instances and are equal to within one second.
|
524
458
|
#
|
525
|
-
#
|
526
|
-
#
|
527
|
-
# and {Java::JavaUtil::Date.from_ruby_date} methods.
|
528
|
-
# A date sent to the database is stored correctly.
|
529
|
-
# However, a date sent to the database does not correctly compare to
|
530
|
-
# the data fetched from the database.
|
531
|
-
# This method accounts for the DST discrepancy when comparing dates.
|
459
|
+
# The DateTime comparison accounts for differences in the Ruby -> Java -> Ruby roundtrip
|
460
|
+
# of a date attribute, which loses the seconds fraction.
|
532
461
|
#
|
533
462
|
# @return whether value and other are equal according to the above tests
|
534
463
|
def self.value_equal?(value, other, matches=nil)
|
@@ -537,7 +466,7 @@ module CaRuby
|
|
537
466
|
elsif value.collection? and other.collection? then
|
538
467
|
collection_value_equal?(value, other, matches)
|
539
468
|
elsif DateTime === value and DateTime === other then
|
540
|
-
value
|
469
|
+
(value - other).abs.floor.zero?
|
541
470
|
elsif Resource === value and value.class === other then
|
542
471
|
value.match?(other)
|
543
472
|
elsif matches then
|
@@ -552,7 +481,7 @@ module CaRuby
|
|
552
481
|
# Adds the default values to this object, if it is not already fetched, and its dependents.
|
553
482
|
def add_defaults_recursive
|
554
483
|
# add the local defaults unless there is an identifier
|
555
|
-
add_defaults_local
|
484
|
+
add_defaults_local
|
556
485
|
# add dependent defaults
|
557
486
|
each_defaults_dependent { |dep| dep.add_defaults_recursive }
|
558
487
|
end
|
@@ -563,7 +492,7 @@ module CaRuby
|
|
563
492
|
# work around a caTissue bug (see that module for details). Other definitions
|
564
493
|
# of this method are discouraged.
|
565
494
|
def missing_mandatory_attributes
|
566
|
-
|
495
|
+
mandatory_attributes.select { |attr| send(attr).nil_or_empty? }
|
567
496
|
end
|
568
497
|
|
569
498
|
private
|
@@ -603,8 +532,8 @@ module CaRuby
|
|
603
532
|
# If a subclass overrides this method, then it should call super before setting the local
|
604
533
|
# default attributes. This ensures that configuration defaults takes precedence.
|
605
534
|
def add_defaults_local
|
535
|
+
logger.debug { "Adding defaults to #{qp}..." }
|
606
536
|
merge_attributes(self.class.defaults)
|
607
|
-
self
|
608
537
|
end
|
609
538
|
|
610
539
|
# Enumerates the dependents for setting defaults. Subclasses can override if the
|
@@ -616,46 +545,9 @@ module CaRuby
|
|
616
545
|
not (attributes.empty? or attributes.any? { |attr| send(attr).nil? })
|
617
546
|
end
|
618
547
|
|
619
|
-
# Returns the source => target hash of matches for the given attr_md newval sources and
|
620
|
-
# oldval targets. If the matcher block is given, then that block is called on the sources
|
621
|
-
# and targets. Otherwise, {Resource.match_all} is called.
|
622
|
-
#
|
623
|
-
# @param [AttributeMetadata] attr_md the attribute to match
|
624
|
-
# @param newval the source value
|
625
|
-
# @param oldval the target value
|
626
|
-
# @yield [sources, targets] matches sources to targets
|
627
|
-
# @yieldparam [<Resource>] sources an Enumerable on the source value
|
628
|
-
# @yieldparam [<Resource>] targets an Enumerable on the target value
|
629
|
-
# @return [{Resource => Resource}] the source => target matches
|
630
|
-
def match_attribute_value(attr_md, newval, oldval)
|
631
|
-
# make Enumerable targets and sources for matching
|
632
|
-
sources = newval.to_enum
|
633
|
-
targets = oldval.to_enum
|
634
|
-
|
635
|
-
# match sources to targets
|
636
|
-
logger.debug { "Matching source #{newval.qp} to target #{qp} #{attr_md} #{oldval.qp}..." }
|
637
|
-
block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
|
638
|
-
end
|
639
|
-
|
640
|
-
# @param [DateTime] d1 the first date
|
641
|
-
# @param [DateTime] d2 the second date
|
642
|
-
# @return whether d1 matches d2 on the non-offset fields
|
643
|
-
# @see #value_equal
|
644
|
-
def self.dates_equal_modulo_dst(d1, d2)
|
645
|
-
d1.strftime('%FT%T') == d1.strftime('%FT%T')
|
646
|
-
end
|
647
|
-
|
648
548
|
def self.collection_value_equal?(value, other, matches=nil)
|
649
549
|
value.size == other.size and value.all? { |v| other.include?(v) or (matches and other.include?(matches[v])) }
|
650
550
|
end
|
651
|
-
|
652
|
-
# @param (see #merge_match)
|
653
|
-
# @return [String] the log message
|
654
|
-
def format_merge_log_message(other, attributes)
|
655
|
-
attr_list = attributes.sort { |a1, a2| a1.to_s <=> a2.to_s }
|
656
|
-
attr_clause = " including domain attributes #{attr_list.to_series}" unless attr_list.empty?
|
657
|
-
"Merging #{other.qp} into #{qp}#{attr_clause}..."
|
658
|
-
end
|
659
551
|
|
660
552
|
# A DetailPrinter formats a domain object value for printing using {#to_s} the first time the object
|
661
553
|
# is encountered and a ReferencePrinter on the object subsequently.
|
@@ -749,68 +641,19 @@ module CaRuby
|
|
749
641
|
end
|
750
642
|
end
|
751
643
|
|
752
|
-
#
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
inv_md = attr_md.inverse_attribute_metadata
|
766
|
-
if inv_md and not inv_md.collection? then
|
767
|
-
owtr = inv_md.writer
|
768
|
-
end
|
769
|
-
|
770
|
-
# if the attribute is a collection, then merge the matches into the current attribute
|
771
|
-
# collection value and add each unmatched source to the collection.
|
772
|
-
# otherwise, if the attribute is not yet set and there is a new value, then set it
|
773
|
-
# to the new value match or the new value itself if unmatched.
|
774
|
-
if attr_md.collection? then
|
775
|
-
# TODO - refactor into method
|
776
|
-
# the references to add
|
777
|
-
adds = []
|
778
|
-
logger.debug { "Merging #{newval.qp} into #{qp} #{attr_md} #{oldval.qp}..." } unless newval.empty?
|
779
|
-
newval.each do |src|
|
780
|
-
# if the match target is in the current collection, then update the matched
|
781
|
-
# target from the source.
|
782
|
-
# otherwise, if there is no match or the match is a new reference created
|
783
|
-
# from the match, then add the match to the oldval collection.
|
784
|
-
if matches.has_key?(src) then
|
785
|
-
# the source match
|
786
|
-
tgt = matches[src]
|
787
|
-
if oldval.include?(tgt) then
|
788
|
-
tgt.merge_attributes(src)
|
789
|
-
else
|
790
|
-
adds << tgt
|
791
|
-
end
|
792
|
-
else
|
793
|
-
adds << src
|
794
|
-
end
|
795
|
-
end
|
796
|
-
# add the unmatched sources
|
797
|
-
logger.debug { "Adding #{qp} #{attr_md} unmatched #{adds.qp}..." } unless adds.empty?
|
798
|
-
adds.each do |ref|
|
799
|
-
# if there is an owner writer attribute, then add the ref to the attribute collection by
|
800
|
-
# delegating to the owner writer. otherwise, add the ref to the attribute collection directly.
|
801
|
-
owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : oldval << ref
|
802
|
-
end
|
803
|
-
elsif newval then
|
804
|
-
if oldval then
|
805
|
-
oldval.merge(newval)
|
806
|
-
else
|
807
|
-
# the target is either a new object created in the match or the source value
|
808
|
-
ref = matches.has_key?(newval) ? matches[newval] : newval
|
809
|
-
# if the target is a dependent, then set the dependent owner, which in turn will
|
810
|
-
# set the attribute to the dependent. otherwise, set attribute to the target.
|
811
|
-
logger.debug { "Setting #{qp} #{attr_md} to #{ref.qp}..." }
|
812
|
-
owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : send(attr_md.writer, ref)
|
813
|
-
end
|
644
|
+
# Returns whether the other domain object matches this domain object on a secondary
|
645
|
+
# key without owner attributes. Defaults are added to this object in order to pick up
|
646
|
+
# potential secondary key values.
|
647
|
+
#
|
648
|
+
# @param (see #match_in)
|
649
|
+
# @return [Boolean] whether the other domain object matches this domain object on a
|
650
|
+
# secondary key without owner attributes
|
651
|
+
def match_without_owner_attribute?(other)
|
652
|
+
oattrs = self.class.owner_attributes
|
653
|
+
return if oattrs.empty?
|
654
|
+
# match on the secondary key
|
655
|
+
self.class.secondary_key_attributes.all? do |attr|
|
656
|
+
oattrs.include?(attr) or matches_attribute_value?(other, attr, send(attr))
|
814
657
|
end
|
815
658
|
end
|
816
659
|
|
@@ -819,102 +662,17 @@ module CaRuby
|
|
819
662
|
ref.send(writer, self)
|
820
663
|
end
|
821
664
|
|
822
|
-
# Sets an exclusive dependent attribute to the given dependent dep.
|
823
|
-
# If dep is not nil, then this method calls the dep inv_writer with argument self
|
824
|
-
# before calling the writer with argument dep.
|
825
|
-
def set_exclusive_dependent(dep, writer, inv_writer)
|
826
|
-
dep.send(inv_writer, self) if dep
|
827
|
-
send(writer, dep)
|
828
|
-
end
|
829
|
-
|
830
|
-
# Sets the inversible attribute with the given accessors and inverse to
|
831
|
-
# the given newval. The inverse of the attribute is a collection accessed by
|
832
|
-
# calling inverse on newval.
|
833
|
-
#
|
834
|
-
# For an attribute +owner+ with writer +setOwner+ and inverse reader +deps+,
|
835
|
-
# this is equivalent to the following:
|
836
|
-
# class Dependent
|
837
|
-
# def owner=(o)
|
838
|
-
# owner.deps.delete(owner) if owner
|
839
|
-
# setOwner(o)
|
840
|
-
# o.deps << self if o
|
841
|
-
# end
|
842
|
-
# end
|
843
|
-
#
|
844
|
-
# @param [Resource] new_ref the new attribute reference value
|
845
|
-
# @param [(Symbol, Symbol)] accessors the reader and writer to use in setting
|
846
|
-
# the attribute
|
847
|
-
# @param [Symbol] inverse the inverse collection attribute to which
|
848
|
-
# this domain object will be added
|
849
|
-
def add_to_inverse_collection(new_ref, accessors, inverse)
|
850
|
-
reader, writer = accessors
|
851
|
-
# the current inverse
|
852
|
-
old_ref = send(reader)
|
853
|
-
# no-op if no change
|
854
|
-
return new_ref if old_ref == new_ref
|
855
|
-
|
856
|
-
# delete self from the current inverse reference collection
|
857
|
-
if old_ref then
|
858
|
-
old_ref.suspend_lazy_loader do
|
859
|
-
oldcoll = old_ref.send(inverse)
|
860
|
-
oldcoll.delete(self) if oldcoll
|
861
|
-
end
|
862
|
-
end
|
863
|
-
|
864
|
-
# call the writer on this object
|
865
|
-
send(writer, new_ref)
|
866
|
-
# add self to the inverse collection
|
867
|
-
if new_ref then
|
868
|
-
new_ref.suspend_lazy_loader do
|
869
|
-
newcoll = new_ref.send(inverse)
|
870
|
-
newcoll << self
|
871
|
-
unless newcoll then
|
872
|
-
raise TypeError.new("Cannot create #{new_ref.qp} #{inverse} collection to hold #{self}")
|
873
|
-
end
|
874
|
-
if old_ref then
|
875
|
-
logger.debug { "Moved #{qp} from #{reader} #{old_ref.qp} #{inverse} to #{new_ref.qp}." }
|
876
|
-
else
|
877
|
-
logger.debug { "Added #{qp} to #{reader} #{new_ref.qp} #{inverse}." }
|
878
|
-
end
|
879
|
-
end
|
880
|
-
end
|
881
|
-
new_ref
|
882
|
-
end
|
883
|
-
|
884
|
-
# Sets the attribute with the given accessors and inverse_writer to the given newval.
|
885
|
-
#
|
886
|
-
# For an attribute +owner+ with writer +setOwner+ and inverse +dep+, this is equivalent
|
887
|
-
# to the following:
|
888
|
-
# class Dependent
|
889
|
-
# def owner=(o)
|
890
|
-
# owner.dep = nil if owner
|
891
|
-
# setOwner(o)
|
892
|
-
# o.dep = self if o
|
893
|
-
# end
|
894
|
-
# end
|
895
|
-
def set_inversible_noncollection_attribute(newval, accessors, inverse_writer)
|
896
|
-
reader, writer = accessors
|
897
|
-
# the previous value
|
898
|
-
oldval = suspend_lazy_loader { send(reader) }
|
899
|
-
# bail if no change
|
900
|
-
return newval if newval.equal?(oldval)
|
901
|
-
|
902
|
-
# clear the previous inverse
|
903
|
-
oldval.send(inverse_writer, nil) if oldval
|
904
|
-
# call the writer
|
905
|
-
send(writer, newval)
|
906
|
-
logger.debug { "Moved #{qp} from #{oldval.qp} to #{newval.qp}." } if oldval and newval
|
907
|
-
# call the inverse writer on self
|
908
|
-
newval.send(inverse_writer, self) if newval
|
909
|
-
newval
|
910
|
-
end
|
911
|
-
|
912
665
|
# Returns 0 if attribute is a Java primitive number,
|
913
|
-
# +false+ if attribute is a Java primitive boolean,
|
666
|
+
# +false+ if attribute is a Java primitive boolean,
|
667
|
+
# an empty collectin if the Java property is a collection,
|
668
|
+
# nil otherwise.
|
914
669
|
def empty_value(attribute)
|
915
|
-
type = java_type(attribute)
|
916
|
-
|
917
|
-
|
670
|
+
type = java_type(attribute) || return
|
671
|
+
if type.primitive? then
|
672
|
+
type.name == 'boolean' ? false : 0
|
673
|
+
else
|
674
|
+
self.class.empty_value(attribute)
|
675
|
+
end
|
918
676
|
end
|
919
677
|
|
920
678
|
# Returns the Java type of the given attribute, or nil if attribute is not a Java property attribute.
|
@@ -923,6 +681,29 @@ module CaRuby
|
|
923
681
|
attr_md.property_descriptor.property_type if JavaAttributeMetadata === attr_md
|
924
682
|
end
|
925
683
|
|
684
|
+
# Returns the source => target hash of matches for the given attr_md newval sources and
|
685
|
+
# oldval targets. If the matcher block is given, then that block is called on the sources
|
686
|
+
# and targets. Otherwise, {Resource.match_all} is called.
|
687
|
+
#
|
688
|
+
# @param [AttributeMetadata] attr_md the attribute to match
|
689
|
+
# @param newval the source value
|
690
|
+
# @param oldval the target value
|
691
|
+
# @yield [sources, targets] matches sources to targets
|
692
|
+
# @yieldparam [<Resource>] sources an Enumerable on the source value
|
693
|
+
# @yieldparam [<Resource>] targets an Enumerable on the target value
|
694
|
+
# @return [{Resource => Resource}] the source => target matches
|
695
|
+
def match_attribute_value(attr_md, newval, oldval)
|
696
|
+
# make Enumerable targets and sources for matching
|
697
|
+
sources = newval.to_enum
|
698
|
+
targets = oldval.to_enum
|
699
|
+
|
700
|
+
# match sources to targets
|
701
|
+
logger.debug { "Matching source #{newval.qp} to target #{qp} #{attr_md} #{oldval.qp}..." } unless oldval.nil_or_empty?
|
702
|
+
matches = block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
|
703
|
+
logger.debug { "Matched #{qp} #{attr_md}: #{matches.qp}." } unless matches.empty?
|
704
|
+
matches
|
705
|
+
end
|
706
|
+
|
926
707
|
# Returns the object in others which uniquely matches this domain object on the given attributes,
|
927
708
|
# or nil if there is no unique match. This method returns nil if any attributes value is nil.
|
928
709
|
def match_unique_object_with_attributes(others, attributes)
|