caruby-core 1.4.2 → 1.4.3
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|