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,11 @@
|
|
1
|
+
require 'caruby/domain/resource_module'
|
2
|
+
|
3
|
+
module CaRuby
|
4
|
+
module ResourceModule
|
5
|
+
# Declares the given classes which will be dynamically modified for migration.
|
6
|
+
# The Java caBIG classes are auto-loaded and wrapped as a CaRuby::Resource, if necessary, and enhanced in the migration shim.
|
7
|
+
def shims(*classes)
|
8
|
+
# nothing to do, since all this method does is ensure that the arguments are auto-loaded when they are referenced
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'caruby/migration/migratable'
|
2
|
+
require 'caruby/domain/uniquify'
|
3
|
+
|
4
|
+
module CaRuby
|
5
|
+
module Migratable
|
6
|
+
# Unique makes a Migratable Resource domain object unique within the scope its class.
|
7
|
+
module Unique
|
8
|
+
include CaRuby::Resource::Unique
|
9
|
+
|
10
|
+
# Augments the migration by making this Resource object unique in the scope of its class.
|
11
|
+
#
|
12
|
+
# @param (see CaRuby::Migratable#migrate)
|
13
|
+
def migrate(row, migrated)
|
14
|
+
super
|
15
|
+
logger.debug { "Migrator making #{self} unique..." }
|
16
|
+
uniquify
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,969 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'caruby/util/inflector'
|
3
|
+
require 'caruby/util/log'
|
4
|
+
require 'caruby/util/pretty_print'
|
5
|
+
require 'caruby/util/validation'
|
6
|
+
require 'caruby/util/collection'
|
7
|
+
require 'caruby/domain/merge'
|
8
|
+
require 'caruby/domain/reference_visitor'
|
9
|
+
require 'caruby/database/persistable'
|
10
|
+
require 'caruby/domain/resource_metadata'
|
11
|
+
require 'caruby/domain/resource_module'
|
12
|
+
|
13
|
+
module CaRuby
|
14
|
+
# The Domain module is included by Java domain classes.
|
15
|
+
# This module defines essential common domain methods that enable the jRuby-Java API bridge.
|
16
|
+
# Classes which include Domain must implement the +metadata+ Domain::Metadata accessor method.
|
17
|
+
module Resource
|
18
|
+
include Comparable, Mergeable, Persistable, Validation
|
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
|
44
|
+
|
45
|
+
# Sets the default attribute values for this domain object and its dependents. If this Resource
|
46
|
+
# does not have an identifier, then missing attributes are set to the values defined by
|
47
|
+
# {ResourceAttributes#add_attribute_defaults}.
|
48
|
+
#
|
49
|
+
# _Implementation Note_: subclasses should override the private {#add_defaults_local} method
|
50
|
+
# rather than this method.
|
51
|
+
#
|
52
|
+
# @return [Resource] self
|
53
|
+
def add_defaults
|
54
|
+
# apply owner defaults
|
55
|
+
if owner then
|
56
|
+
owner.add_defaults
|
57
|
+
else
|
58
|
+
logger.debug { "Adding defaults to #{qp} and its dependents..." }
|
59
|
+
# apply the local and dependent defaults
|
60
|
+
add_defaults_recursive
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Validates this domain object and its cascaded dependents for completeness prior to a
|
66
|
+
# database create or update operation.
|
67
|
+
# The object is valid if it contains a non-nil value for each mandatory property.
|
68
|
+
# Objects which have already been validated are skipped.
|
69
|
+
# Returns this domain object.
|
70
|
+
#
|
71
|
+
# @raise [ValidationError] if a mandatory attribute value is missing
|
72
|
+
def validate
|
73
|
+
unless @validated then
|
74
|
+
logger.debug { "Validating #{qp} required attributes #{self.class.mandatory_attributes.to_a.to_series}..." }
|
75
|
+
invalid = missing_mandatory_attributes
|
76
|
+
unless invalid.empty? then
|
77
|
+
logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
|
78
|
+
raise ValidationError.new("Required attribute value missing for #{self}: #{invalid.join(', ')}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
self.class.cascaded_attributes.each do |attr|
|
82
|
+
send(attr).enumerate { |dep| dep.validate }
|
83
|
+
end
|
84
|
+
@validated = true
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the attributes to use for a search using this domain object as a template, determined
|
89
|
+
# as follows:
|
90
|
+
# * If this domain object has a non-nil primary key, then the primary key is the search criterion.
|
91
|
+
# * Otherwise, if this domain object has a secondary key and each key attribute value is not nil,
|
92
|
+
# then the secondary key is the search criterion.
|
93
|
+
# * Otherwise, if this domain object has an alternate key and each key attribute value is not nil,
|
94
|
+
# then the aklternate key is the search criterion.
|
95
|
+
#
|
96
|
+
# @return [<Symbol>] the attributes to use for a search on this domain object
|
97
|
+
def searchable_attributes
|
98
|
+
key_attrs = self.class.primary_key_attributes
|
99
|
+
return key_attrs if key_searchable?(key_attrs)
|
100
|
+
key_attrs = self.class.secondary_key_attributes
|
101
|
+
return key_attrs if key_searchable?(key_attrs)
|
102
|
+
key_attrs = self.class.alternate_key_attributes
|
103
|
+
return key_attrs if key_searchable?(key_attrs)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns a new domain object with the given attributes copied from this domain object. The attributes
|
107
|
+
# argument consists of either attribute Symbols or a single Enumerable consisting of Symbols.
|
108
|
+
# The default attributes are the {ResourceAttributes#nondomain_attributes}.
|
109
|
+
#
|
110
|
+
# @param [<Symbol>] attributes the attributes to copy
|
111
|
+
# @return [Resource] a copy of this domain object
|
112
|
+
def copy(*attributes)
|
113
|
+
if attributes.empty? then
|
114
|
+
attributes = self.class.nondomain_attributes
|
115
|
+
elsif Enumerable === attributes.first then
|
116
|
+
raise ArgumentError.new("#{qp} copy attributes argument is not a Symbol: #{attributes.first}") unless attributes.size == 1
|
117
|
+
attributes = attributes.first
|
118
|
+
end
|
119
|
+
self.class.new.merge_attributes(self, attributes)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Merges the other into this domain object. The non-domain attributes are always merged.
|
123
|
+
#
|
124
|
+
# @param [Resource] other the merge source domain object
|
125
|
+
# @param [<Symbol>] attributes the domain attributes to merge
|
126
|
+
# @yield [source, target] mergethe source into the target
|
127
|
+
# @yieldparam [Resource] source the reference to merge from
|
128
|
+
# @yieldparam [Resource] target the reference the source is merge into
|
129
|
+
# @raise [ArgumentError] if other is not an instance of this domain object's class
|
130
|
+
# @see #merge_attribute_value
|
131
|
+
def merge_match(other, attributes, &matcher)
|
132
|
+
raise ArgumentError.new("Incompatible #{qp} merge source: #{other.qp}") unless self.class === other
|
133
|
+
logger.debug { format_merge_log_message(other, attributes) }
|
134
|
+
# merge the non-domain attributes
|
135
|
+
merge_attributes(other)
|
136
|
+
# merge the domain attributes
|
137
|
+
unless attributes.nil_or_empty? then
|
138
|
+
merge_attributes(other, attributes) do |attr, oldval, newval|
|
139
|
+
merge_attribute_value(attr, oldval, newval, &matcher)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Merges the attribute newval into oldval as follows:
|
145
|
+
# * If attribute is a non-domain attribute and oldval is either nil or the attribute is not saved,
|
146
|
+
# then the attribute value is set to newval.
|
147
|
+
# * Otherwise, if attribute is a domain non-collection attribute, then newval is recursively
|
148
|
+
# merged into oldval.
|
149
|
+
# * Otherwise, if attribute is a domain collection attribute, then matching newval members are
|
150
|
+
# merged into the corresponding oldval member and non-matching newval members are added to
|
151
|
+
# newval.
|
152
|
+
# * Otherwise, the attribute value is not changed.
|
153
|
+
#
|
154
|
+
# The domain value match is performed by the matcher block. The block arguments are newval and
|
155
|
+
# oldval and the return value is a source => target hash of matching references. The default
|
156
|
+
# matcher is {Resource#match_in}.
|
157
|
+
#
|
158
|
+
# @param [Symbol] attribute the merge attribute
|
159
|
+
# @param oldval the current value
|
160
|
+
# @param newval the value to merge
|
161
|
+
# @yield [newval, oldval] the value matcher used if attribute is a domain collection
|
162
|
+
# @yieldparam newval the merge source value
|
163
|
+
# @yieldparam oldval this domain object's current attribute value
|
164
|
+
# @return the merged attribute value
|
165
|
+
def merge_attribute_value(attribute, oldval, newval, &matcher)
|
166
|
+
attr_md = self.class.attribute_metadata(attribute)
|
167
|
+
if attr_md.nondomain? then
|
168
|
+
if oldval.nil? and not newval.nil? then
|
169
|
+
send(attr_md.writer, newval)
|
170
|
+
newval
|
171
|
+
end
|
172
|
+
else
|
173
|
+
merge_domain_attribute_value(attr_md, oldval, newval, &matcher)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def clear_attribute(attribute)
|
178
|
+
# the current value to clear
|
179
|
+
current = send(attribute)
|
180
|
+
return if current.nil?
|
181
|
+
# call the current value clear if possible.
|
182
|
+
# otherwise, set the attribute to the empty value.
|
183
|
+
if current.respond_to?(:clear) then
|
184
|
+
current.clear
|
185
|
+
else
|
186
|
+
send(self.class.attribute_metadata(attribute).writer, empty_value(attribute))
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Sets this domain object's attribute to the value. This method clears the current attribute value,
|
191
|
+
# if any, and merges the new value. Merge rather than assignment ensures that a collection type
|
192
|
+
# is preserved, e.g. an Array value is assigned to a set domain type by first clearing the set
|
193
|
+
# and then merging the array content into the set.
|
194
|
+
#
|
195
|
+
# @see #merge_attribute
|
196
|
+
def set_attribute(attribute, value)
|
197
|
+
# bail out if the value argument is the current value
|
198
|
+
return value if value.equal?(send(attribute))
|
199
|
+
clear_attribute(attribute)
|
200
|
+
merge_attribute(attribute, value)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Returns the secondary key attribute values as follows:
|
204
|
+
# * If there is no secondary key, then this method returns nil.
|
205
|
+
# * Otherwise, if the secondary key attributes is a singleton Array, then the key is the value of the sole key attribute.
|
206
|
+
# * Otherwise, the key is an Array of the key attribute values.
|
207
|
+
def key
|
208
|
+
attrs = self.class.secondary_key_attributes
|
209
|
+
case attrs.size
|
210
|
+
when 0 then nil
|
211
|
+
when 1 then send(attrs.first)
|
212
|
+
else attrs.map { |attr| send(attr) }
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the domain object that owns this object, or nil if this object is not dependent on an owner.
|
217
|
+
def owner
|
218
|
+
self.class.owner_attributes.detect_value { |attr| send(attr) }
|
219
|
+
end
|
220
|
+
|
221
|
+
# Returns whether the other domain object is this object's #owner or an #owner_ancestor? of this object's {#owner}.
|
222
|
+
def owner_ancestor?(other)
|
223
|
+
ref = owner
|
224
|
+
ref and (ref == other or ref.owner_ancestor?(other))
|
225
|
+
end
|
226
|
+
|
227
|
+
# Returns an attribute => value hash for the specified attributes with a non-nil, non-empty value.
|
228
|
+
# The default attributes are this domain object's class {ResourceAttributes#attributes}.
|
229
|
+
# Only non-nil attributes defined by this Resource are included in the result hash.
|
230
|
+
#
|
231
|
+
# @param [<Symbol>, nil] attributes the attributes to merge
|
232
|
+
# @return [{Symbol => Object}] the attribute => value hash
|
233
|
+
def value_hash(attributes=nil)
|
234
|
+
attributes ||= self.class.attributes
|
235
|
+
attributes.to_compact_hash { |attr| send(attr) if self.class.method_defined?(attr) }
|
236
|
+
end
|
237
|
+
|
238
|
+
# Returns the domain object references for the given attributes.
|
239
|
+
#
|
240
|
+
# @param [<Symbol>, nil] the domain attributes to include, or nil to include all domain attributes
|
241
|
+
# @return [<Resource>] the referenced attribute domain object values
|
242
|
+
def references(attributes=nil)
|
243
|
+
attributes ||= self.class.domain_attributes
|
244
|
+
attributes.map { |attr| send(attr) }.flatten.compact
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns whether this domain object is dependent on another entity.
|
248
|
+
def dependent?
|
249
|
+
self.class.dependent?
|
250
|
+
end
|
251
|
+
|
252
|
+
# Returns whether this domain object is not dependent on another entity.
|
253
|
+
def independent?
|
254
|
+
not dependent?
|
255
|
+
end
|
256
|
+
|
257
|
+
# Enumerates over this domain object's dependents.
|
258
|
+
#
|
259
|
+
# @yield [dep] the block to execute on the dependent
|
260
|
+
# @yieldparam [Resource] dep the dependent
|
261
|
+
def each_dependent
|
262
|
+
self.class.dependent_attributes.each do |attr|
|
263
|
+
send(attr).enumerate { |dep| yield dep }
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# The dependent Enumerable.
|
268
|
+
def dependents
|
269
|
+
enum_for(:each_dependent)
|
270
|
+
end
|
271
|
+
|
272
|
+
# Returns the attribute references which directly depend on this owner.
|
273
|
+
# The default is the attribute value.
|
274
|
+
#
|
275
|
+
# Returns an Enumerable. If the value is not already an Enumerable, then this method
|
276
|
+
# returns an empty array if value is nil, or a singelton array with value otherwise.
|
277
|
+
#
|
278
|
+
# If there is more than one owner of a dependent, then subclasses should override this
|
279
|
+
# method to select dependents whose dependency path is shorter than an alternative
|
280
|
+
# dependency path, e.g. in caTissue a Specimen is owned by both a SCG and a parent
|
281
|
+
# Specimen. In that case, the SCG direct dependents consist of top-level Specimens
|
282
|
+
# owned by the SCG but not derived from another Specimen.
|
283
|
+
def direct_dependents(attribute)
|
284
|
+
deps = send(attribute)
|
285
|
+
case deps
|
286
|
+
when Enumerable then deps
|
287
|
+
when nil then Array::EMPTY_ARRAY
|
288
|
+
else [deps]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Returns pairs +[+_value1_, _value2_+]+ of correlated non-nil values for every attribute in attributes
|
293
|
+
# in this and the other domain object, e.g. given domain objects +site1+ and +site2+ with non-nil
|
294
|
+
# +address+ dependents:
|
295
|
+
# site1.assoc_attribute_values(site2, [:address]) #=> [site1.address, site2.address]
|
296
|
+
def assoc_attribute_values(other, attributes)
|
297
|
+
return {} if other.nil? or attributes.empty?
|
298
|
+
# associate the attribute => value hashes for this object and the other hash into one attribute => [value1, value2] hash
|
299
|
+
value_hash(attributes).assoc_values(other.value_hash(attributes)).values.reject { |values| values.any? { |value| value.nil? } }
|
300
|
+
end
|
301
|
+
|
302
|
+
# Returns whether this object matches the fetched other object on class and key values.
|
303
|
+
def match?(other)
|
304
|
+
match_in([other])
|
305
|
+
end
|
306
|
+
|
307
|
+
# Matches this dependent domain object with the others on type and key attributes
|
308
|
+
# in the scope of a parent object.
|
309
|
+
# Returns the object in others which matches this domain object, or nil if none.
|
310
|
+
#
|
311
|
+
# The match attributes are, in order:
|
312
|
+
# * the primary key
|
313
|
+
# * the secondary key
|
314
|
+
# * the alternate key
|
315
|
+
#
|
316
|
+
# This domain object is matched against the others on the above attributes in succession
|
317
|
+
# until a unique match is found. The key attribute matches are strict, i.e. each
|
318
|
+
# key attribute value must be non-nil and match the other value.
|
319
|
+
def match_in(others)
|
320
|
+
# trivial case: self is in others
|
321
|
+
return self if others.include?(self)
|
322
|
+
# filter for the same type
|
323
|
+
others = others.filter { |other| self.class === other }
|
324
|
+
# match on primary, secondary or alternate key
|
325
|
+
match_unique_object_with_attributes(others, self.class.primary_key_attributes) or
|
326
|
+
match_unique_object_with_attributes(others, self.class.secondary_key_attributes) or
|
327
|
+
match_unique_object_with_attributes(others, self.class.alternate_key_attributes)
|
328
|
+
end
|
329
|
+
|
330
|
+
# Returns the match of this domain object in the scope of a matching owner, if any.
|
331
|
+
# If this domain object is dependent, then the match is performed in the context of a
|
332
|
+
# matching owner object. If {#match_in} returns a match, then that result is used.
|
333
|
+
# Otherwise, #match_without_owner_attribute is called.
|
334
|
+
#
|
335
|
+
# @param [<Resource>] others the candidate domain objects for the match
|
336
|
+
# @return [Resource, nil] the matching domain object, if any
|
337
|
+
def match_in_owner_scope(others)
|
338
|
+
match_in(others) or others.detect { |other| match_without_owner_attribute(other) }
|
339
|
+
end
|
340
|
+
|
341
|
+
# @param [<Resource>] other the domain object to match against
|
342
|
+
# @return [Boolean] whether the other domain object matches this domain object on a secondary
|
343
|
+
# key without owner attributes
|
344
|
+
def match_without_owner_attribute(other)
|
345
|
+
oattrs = self.class.owner_attributes
|
346
|
+
return if oattrs.empty?
|
347
|
+
self.class.secondary_key_attributes.all? do |attr|
|
348
|
+
matches_attribute_value?(other, attr, send(attr))
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# @return [{Resouce => Resource}] a source => target hash of the given sources which match the
|
353
|
+
# targets using the {#match_in} method
|
354
|
+
def self.match_all(sources, targets)
|
355
|
+
DEF_MATCHER.match(sources, targets)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Returns the difference between this Persistable and the other Persistable for the
|
359
|
+
# given attributes. The default attributes are the {ResourceAttributes#nondomain_attributes}.
|
360
|
+
#
|
361
|
+
# @param [Resource] other the domain object to compare
|
362
|
+
# @param [<Symbol>, nil] attributes the attributes to compare
|
363
|
+
# @return (see Hashable#diff)
|
364
|
+
def diff(other, attributes=nil)
|
365
|
+
attributes ||= self.class.nondomain_attributes
|
366
|
+
vh = value_hash(attributes)
|
367
|
+
ovh = other.value_hash(attributes)
|
368
|
+
vh.diff(ovh) { |key, v1, v2| Resource.value_equal?(v1, v2) }
|
369
|
+
end
|
370
|
+
|
371
|
+
# Returns the domain object in others which matches this dependent domain object
|
372
|
+
# within the scope of a parent on a minimally acceptable constraint. This method
|
373
|
+
# is used when this object might be partially complete--say, lacking a secondary key
|
374
|
+
# value--but is expected to match one of the others, e.g. when matching a referenced
|
375
|
+
# object to its fetched counterpart.
|
376
|
+
#
|
377
|
+
# This base implementation returns whether the following conditions hold:
|
378
|
+
# 1. other is the same class as this domain object
|
379
|
+
# 2. if both identifiers are non-nil, then they are equal
|
380
|
+
#
|
381
|
+
# Subclasses can override this method to impose additional minimal consistency constraints.
|
382
|
+
#
|
383
|
+
# @param [Resource] other the domain object to match against
|
384
|
+
# @return [Boolean] whether this Resource equals other
|
385
|
+
def minimal_match?(other)
|
386
|
+
self.class === other and
|
387
|
+
(identifier.nil? or other.identifier.nil? or identifier == other.identifier)
|
388
|
+
end
|
389
|
+
|
390
|
+
# Returns an enumerator on the transitive closure of the reference attributes.
|
391
|
+
# If a block is given to this method, then the block called on each reference determines
|
392
|
+
# which attributes to visit. Otherwise, all saved references are visited.
|
393
|
+
#
|
394
|
+
# @yield [ref] reference visit attribute selector
|
395
|
+
# @yieldparam [Resource] ref the domain object to visit
|
396
|
+
# @return [Enumerable] the reference transitive closure
|
397
|
+
def reference_hierarchy
|
398
|
+
ReferenceVisitor.new { |ref| yield ref }.to_enum(self)
|
399
|
+
end
|
400
|
+
|
401
|
+
# Returns the value for the given attribute path Array or String expression, e.g.:
|
402
|
+
# study.path_value("site.address.state")
|
403
|
+
# follows the +study+ -> +site+ -> +address+ -> +state+ accessors and returns the +state+
|
404
|
+
# value, or nil if any intermediate reference is nil.
|
405
|
+
# The array form for the above example is:
|
406
|
+
# study.path_value([:site, :address, :state])
|
407
|
+
#
|
408
|
+
# @param [<Symbol>] path the attributes to navigate
|
409
|
+
# @return the attribute navigation result
|
410
|
+
def path_value(path)
|
411
|
+
path = path.split('.').map { |attr| attr.to_sym } if String === path
|
412
|
+
path.inject(self) do |parent, attr|
|
413
|
+
value = parent.send(attr)
|
414
|
+
return if value.nil?
|
415
|
+
value
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Applies the operator block to this object and each domain object in the reference path.
|
420
|
+
# This method visits the transitive closure of each recursive path attribute.
|
421
|
+
#
|
422
|
+
# For example, given the attributes:
|
423
|
+
# treatment: BioMaterial -> Treatment
|
424
|
+
# measurement: Treatment -> BioMaterial
|
425
|
+
# and +BioMaterial+ instance +biospecimen+, then:
|
426
|
+
# biospecimen.visit_path[:treatment, :measurement, :biomaterial]
|
427
|
+
# visits +biospecimen+ and all biomaterial, treatments and measurements derived
|
428
|
+
# directly or indirectly from +biospecimen+.
|
429
|
+
#
|
430
|
+
# @param [<Symbol>] path the attributes to visit
|
431
|
+
# @yieldparam [Symbol] attribute the attribute to visit
|
432
|
+
# @return the visit result
|
433
|
+
def visit_path(path, &operator)
|
434
|
+
visitor = ReferencePathVisitorFactory.create(self.class, path)
|
435
|
+
visitor.visit(self, &operator)
|
436
|
+
end
|
437
|
+
|
438
|
+
# Applies the operator block to the transitive closure of this domain object's dependency relation.
|
439
|
+
# The block argument is a dependent.
|
440
|
+
#
|
441
|
+
# @yield [dep] operation on the visited domain object
|
442
|
+
# @yieldparam [Resource] dep the domain object to visit
|
443
|
+
def visit_dependents(&operator) # :yields: dependent
|
444
|
+
DEPENDENT_VISITOR.visit(self, &operator)
|
445
|
+
end
|
446
|
+
|
447
|
+
# Applies the operator block to the transitive closure of this domain object's owner relation.
|
448
|
+
#
|
449
|
+
# @yield [dep] operation on the visited domain object
|
450
|
+
# @yieldparam [Resource] dep the domain object to visit
|
451
|
+
def visit_owners(&operator) # :yields: owner
|
452
|
+
ref = owner
|
453
|
+
yield(ref) and ref.visit_owners(&operator) if ref
|
454
|
+
end
|
455
|
+
|
456
|
+
def pretty_print(q)
|
457
|
+
q.text(qp)
|
458
|
+
content = printable_content
|
459
|
+
q.pp_hash(content) unless content.empty?
|
460
|
+
end
|
461
|
+
|
462
|
+
# Prints this domain object's content and recursively prints the referenced content.
|
463
|
+
# The optional selector block determines the attributes to print. The default is all
|
464
|
+
# Java domain attributes.
|
465
|
+
#
|
466
|
+
# @yield [owner] the owner attribute selector
|
467
|
+
# @yieldparam [Resource] owner the domain object to print
|
468
|
+
# @return [String] the domain object content
|
469
|
+
def dump(&selector)
|
470
|
+
DetailPrinter.new(self, &selector).pp_s
|
471
|
+
end
|
472
|
+
|
473
|
+
# Prints this domain object in the format:
|
474
|
+
# class_name@object_id{attribute => value ...}
|
475
|
+
# The default attributes include identifying attributes.
|
476
|
+
#
|
477
|
+
# @param [<Symbol>] attributes the attributes to print
|
478
|
+
# @return [String] the formatted content
|
479
|
+
def to_s(attributes=nil)
|
480
|
+
content = printable_content(attributes)
|
481
|
+
content_s = content.pp_s(:single_line) unless content.empty?
|
482
|
+
"#{print_class_and_id}#{content_s}"
|
483
|
+
end
|
484
|
+
|
485
|
+
alias :inspect :to_s
|
486
|
+
|
487
|
+
# Returns this domain object's attributes content as an attribute => value hash
|
488
|
+
# suitable for printing.
|
489
|
+
#
|
490
|
+
# The default attributes are this object's saved attributes. The optional
|
491
|
+
# reference_printer is used to print a referenced domain object.
|
492
|
+
#
|
493
|
+
# @param [<Symbol>, nil] attributes the attributes to print
|
494
|
+
# @yield [ref] the reference print formatter
|
495
|
+
# @yieldparam [Rresource] ref the referenced domain object to print
|
496
|
+
# @return [{Symbol => String}] the attribute => content hash
|
497
|
+
def printable_content(attributes=nil, &reference_printer) # :yields: reference
|
498
|
+
attributes ||= printworthy_attributes
|
499
|
+
vh = suspend_lazy_loader { value_hash(attributes) }
|
500
|
+
vh.transform { |value| printable_value(value, &reference_printer) }
|
501
|
+
end
|
502
|
+
|
503
|
+
# Returns whether value equals other modulo the given matches according to the following tests:
|
504
|
+
# * _value_ == _other_
|
505
|
+
# * _value_ and _other_ are Ruby DateTime instances and _value_ equals _other_,
|
506
|
+
# modulo the Java-Ruby DST differences described in the following paragraph
|
507
|
+
# * _value_ == matches[_other_]
|
508
|
+
# * _value_ and _other_ are Resource instances and _value_ is a {#match?} with _other_.
|
509
|
+
# * _value_ and _other_ are Enumerable with members equal according to the above conditions.
|
510
|
+
# The DateTime comparison accounts for differences in the Ruby -> Java -> Ruby roundtrip
|
511
|
+
# of a date attribute.
|
512
|
+
#
|
513
|
+
# Ruby alert - Unlike Java, Ruby does not adjust the offset for DST.
|
514
|
+
# This adjustment is made in the {Java::JavaUtil::Date#to_ruby_date}
|
515
|
+
# and {Java::JavaUtil::Date.from_ruby_date} methods.
|
516
|
+
# A date sent to the database is stored correctly.
|
517
|
+
# However, a date sent to the database does not correctly compare to
|
518
|
+
# the data fetched from the database.
|
519
|
+
# This method accounts for the DST discrepancy when comparing dates.
|
520
|
+
#
|
521
|
+
# @return whether value and other are equal according to the above tests
|
522
|
+
def self.value_equal?(value, other, matches=nil)
|
523
|
+
if value == other then
|
524
|
+
true
|
525
|
+
elsif value.collection? and other.collection? then
|
526
|
+
collection_value_equal?(value, other, matches)
|
527
|
+
elsif DateTime === value and DateTime === other then
|
528
|
+
value == other or dates_equal_modulo_dst(value, other)
|
529
|
+
elsif Resource === value and value.class === other then
|
530
|
+
value.match?(other)
|
531
|
+
elsif matches then
|
532
|
+
matches[value] == other
|
533
|
+
else
|
534
|
+
false
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
protected
|
539
|
+
|
540
|
+
# Adds the default values to this object, if it is not already fetched, and its dependents.
|
541
|
+
def add_defaults_recursive
|
542
|
+
# add the local defaults unless there is an identifier
|
543
|
+
add_defaults_local unless identifier
|
544
|
+
# add dependent defaults
|
545
|
+
each_defaults_dependent { |dep| dep.add_defaults_recursive }
|
546
|
+
end
|
547
|
+
|
548
|
+
# Returns the required attributes for this domain object which are nil or empty.
|
549
|
+
#
|
550
|
+
# This method is in protected scope to allow the +CaTissue+ domain module to
|
551
|
+
# work around a caTissue bug (see that module for details). Other definitions
|
552
|
+
# of this method are discouraged.
|
553
|
+
def missing_mandatory_attributes
|
554
|
+
self.class.mandatory_attributes.select { |attr| send(attr).nil_or_empty? }
|
555
|
+
end
|
556
|
+
|
557
|
+
private
|
558
|
+
|
559
|
+
# The copy merge call options.
|
560
|
+
COPY_MERGE_OPTS = {:inverse => false}
|
561
|
+
|
562
|
+
# The dependent attribute visitor.
|
563
|
+
#
|
564
|
+
# @see #visit_dependents
|
565
|
+
DEPENDENT_VISITOR = CaRuby::ReferenceVisitor.new { |obj| obj.class.dependent_attributes }
|
566
|
+
|
567
|
+
# Matches the given targets to sources using {Resource#match_in}.
|
568
|
+
class Matcher
|
569
|
+
def match(sources, targets)
|
570
|
+
unmatched = Set === sources ? sources.dup : sources.to_set
|
571
|
+
matches = {}
|
572
|
+
targets.each do |tgt|
|
573
|
+
src = tgt.match_in(unmatched)
|
574
|
+
if src then
|
575
|
+
unmatched.delete(src)
|
576
|
+
matches[src] = tgt
|
577
|
+
end
|
578
|
+
end
|
579
|
+
matches
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
DEF_MATCHER = Matcher.new
|
584
|
+
|
585
|
+
# Sets the default attribute values for this domain object. Unlike {#add_defaults}, this
|
586
|
+
# method does not set defaults for dependents. This method sets the configuration values
|
587
|
+
# for this domain object as described in {#add_defaults}, but does not set defaults for
|
588
|
+
# dependents.
|
589
|
+
#
|
590
|
+
# This method is the integration point for subclasses to augment defaults with programmatic logic.
|
591
|
+
# If a subclass overrides this method, then it should call super before setting the local
|
592
|
+
# default attributes. This ensures that configuration defaults takes precedence.
|
593
|
+
def add_defaults_local
|
594
|
+
merge_attributes(self.class.defaults)
|
595
|
+
self
|
596
|
+
end
|
597
|
+
|
598
|
+
# Enumerates the dependents for setting defaults. Subclasses can override if the
|
599
|
+
# dependents must be visited in a certain order.
|
600
|
+
alias :each_defaults_dependent :each_dependent
|
601
|
+
|
602
|
+
# @return [Boolean] whether the given key attributes is non-empty and each attribute in the key has a non-nil value
|
603
|
+
def key_searchable?(attributes)
|
604
|
+
not (attributes.empty? or attributes.any? { |attr| send(attr).nil? })
|
605
|
+
end
|
606
|
+
|
607
|
+
# Returns the source => target hash of matches for the given attr_md newval sources and
|
608
|
+
# oldval targets. If the matcher block is given, then that block is called on the sources
|
609
|
+
# and targets. Otherwise, {Resource.match_all} is called.
|
610
|
+
#
|
611
|
+
# @param [AttributeMetadata] attr_md the attribute to match
|
612
|
+
# @param newval the source value
|
613
|
+
# @param oldval the target value
|
614
|
+
# @yield [sources, targets] matches sources to targets
|
615
|
+
# @yieldparam [<Resource>] sources an Enumerable on the source value
|
616
|
+
# @yieldparam [<Resource>] targets an Enumerable on the target value
|
617
|
+
# @return [{Resource => Resource}] the source => target matches
|
618
|
+
def match_attribute_value(attr_md, newval, oldval)
|
619
|
+
# make Enumerable targets and sources for matching
|
620
|
+
sources = newval.to_enum
|
621
|
+
targets = oldval.to_enum
|
622
|
+
|
623
|
+
# match sources to targets
|
624
|
+
logger.debug { "Matching source #{newval.qp} to target #{qp} #{attr_md} #{oldval.qp}..." }
|
625
|
+
block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
|
626
|
+
end
|
627
|
+
|
628
|
+
# @param [DateTime] d1 the first date
|
629
|
+
# @param [DateTime] d2 the second date
|
630
|
+
# @return whether d1 matches d2 on the non-offset fields
|
631
|
+
# @see #value_equal
|
632
|
+
def self.dates_equal_modulo_dst(d1, d2)
|
633
|
+
d1.strftime('%FT%T') == d1.strftime('%FT%T')
|
634
|
+
end
|
635
|
+
|
636
|
+
def self.collection_value_equal?(value, other, matches=nil)
|
637
|
+
value.size == other.size and value.all? { |v| other.include?(v) or (matches and other.include?(matches[v])) }
|
638
|
+
end
|
639
|
+
|
640
|
+
# @param (see #merge_match)
|
641
|
+
# @return [String] the log message
|
642
|
+
def format_merge_log_message(other, attributes)
|
643
|
+
attr_list = attributes.sort { |a1, a2| a1.to_s <=> a2.to_s }
|
644
|
+
attr_clause = " including domain attributes #{attr_list.to_series}" unless attr_list.empty?
|
645
|
+
"Merging #{other.qp} into #{qp}#{attr_clause}..."
|
646
|
+
end
|
647
|
+
|
648
|
+
# A DetailPrinter formats a domain object value for printing using {#to_s} the first time the object
|
649
|
+
# is encountered and a ReferencePrinter on the object subsequently.
|
650
|
+
class DetailPrinter
|
651
|
+
alias :to_s :pp_s
|
652
|
+
|
653
|
+
alias :inspect :to_s
|
654
|
+
|
655
|
+
# Creates a DetailPrinter on the base object.
|
656
|
+
def initialize(base, visited=Set.new, &selector)
|
657
|
+
@base = base
|
658
|
+
@visited = visited << base
|
659
|
+
@selector = selector || Proc.new { |ref| ref.class.java_attributes }
|
660
|
+
end
|
661
|
+
|
662
|
+
def pretty_print(q)
|
663
|
+
q.text(@base.qp)
|
664
|
+
# pretty-print the standard attribute values
|
665
|
+
attrs = @selector.call(@base)
|
666
|
+
content = @base.printable_content(attrs) do |ref|
|
667
|
+
@visited.include?(ref) ? ReferencePrinter.new(ref) : DetailPrinter.new(ref, @visited) { |ref| @selector.call(ref) }
|
668
|
+
end
|
669
|
+
q.pp_hash(content)
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
# A ReferencePrinter formats a reference domain object value for printing with just the class and Ruby object_id.
|
674
|
+
class ReferencePrinter
|
675
|
+
extend Forwardable
|
676
|
+
|
677
|
+
def_delegator(:@base, :qp, :to_s)
|
678
|
+
|
679
|
+
alias :inspect :to_s
|
680
|
+
|
681
|
+
# Creates a ReferencePrinter on the base object.
|
682
|
+
def initialize(base)
|
683
|
+
@base = base
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
# Returns a value suitable for printing. If value is a domain object, then the block provided to this method is called.
|
688
|
+
# The default block creates a new ReferencePrinter on the value.
|
689
|
+
def printable_value(value, &reference_printer)
|
690
|
+
Collector.on(value) do |item|
|
691
|
+
if Resource === item then
|
692
|
+
block_given? ? yield(item) : printable_value(item) { |ref| ReferencePrinter.new(ref) }
|
693
|
+
else
|
694
|
+
item
|
695
|
+
end
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
# Returns an attribute => value hash for the +identifier+ attribute, if there is a non_nil +identifier+,
|
700
|
+
# If +identifier+ is nil, then this method returns the secondary key attributes, if they exist,
|
701
|
+
# or the mergeable attributes otherwise. If this is a dependent object, then the owner attribute is
|
702
|
+
# removed from the returned array.
|
703
|
+
def printworthy_attributes
|
704
|
+
return self.class.primary_key_attributes if identifier
|
705
|
+
attrs = self.class.secondary_key_attributes
|
706
|
+
attrs = self.class.nondomain_java_attributes if attrs.empty?
|
707
|
+
attrs = self.class.fetched_attributes if attrs.empty?
|
708
|
+
attrs
|
709
|
+
end
|
710
|
+
|
711
|
+
# Substitutes attribute with the standard attribute and a Java non-Domain instance value with a Domain object if necessary.
|
712
|
+
#
|
713
|
+
# Returns the [standard attribute, standard value] array.
|
714
|
+
def standardize_attribute_value(attribute, value)
|
715
|
+
attr_md = self.class.attribute_metadata(attribute)
|
716
|
+
if attr_md.nil? then
|
717
|
+
raise ArgumentError.new("#{attribute} is neither a #{self.class.qp} standard attribute nor an alias for a standard attribute")
|
718
|
+
end
|
719
|
+
# standardize the value if necessary
|
720
|
+
std_val = attr_md.type && attr_md.type < Resource ? standardize_domain_value(value) : value
|
721
|
+
[attr_md.to_sym, std_val]
|
722
|
+
end
|
723
|
+
|
724
|
+
# Returns a Domain object for a Java non-Domain instance value.
|
725
|
+
def standardize_domain_value(value)
|
726
|
+
if value.nil? or Resource === value then
|
727
|
+
value
|
728
|
+
elsif Enumerable === value then
|
729
|
+
# value is a collection; if value is a nested collection (highly unlikely), then recursively standarize
|
730
|
+
# the value collection members. otherwise, leave the value alone.
|
731
|
+
value.empty? || Resource === value.first ? value : value.map { |item| standardize_domain_value(item) }
|
732
|
+
else
|
733
|
+
# return a new Domain object built from the source Java domain object
|
734
|
+
# (unlikely unless value is a weird toxic Hibernate proxy)
|
735
|
+
logger.debug { "Creating standard domain object from #{value}..." }
|
736
|
+
Domain.const_get(value.class.qp).new.merge_attributes(value)
|
737
|
+
end
|
738
|
+
end
|
739
|
+
|
740
|
+
# @see #merge_attribute_value
|
741
|
+
def merge_domain_attribute_value(attr_md, oldval, newval, &matcher)
|
742
|
+
# the source => target matches
|
743
|
+
matches = match_attribute_value(attr_md, newval, oldval, &matcher)
|
744
|
+
logger.debug { "Matched #{qp} #{attr_md}: #{matches.qp}." } unless matches.empty?
|
745
|
+
merge_matching_attribute_references(attr_md, oldval, newval, matches)
|
746
|
+
# return the merged result
|
747
|
+
send(attr_md.reader)
|
748
|
+
end
|
749
|
+
|
750
|
+
# @see #merge_attribute_value
|
751
|
+
def merge_matching_attribute_references(attr_md, oldval, newval, matches)
|
752
|
+
# the dependent owner writer method, if any
|
753
|
+
inv_md = attr_md.inverse_attribute_metadata
|
754
|
+
if inv_md and not inv_md.collection? then
|
755
|
+
owtr = inv_md.writer
|
756
|
+
end
|
757
|
+
|
758
|
+
# if the attribute is a collection, then merge the matches into the current attribute
|
759
|
+
# collection value and add each unmatched source to the collection.
|
760
|
+
# otherwise, if the attribute is not yet set and there is a new value, then set it
|
761
|
+
# to the new value match or the new value itself if unmatched.
|
762
|
+
if attr_md.collection? then
|
763
|
+
# TODO - refactor into method
|
764
|
+
# the references to add
|
765
|
+
adds = []
|
766
|
+
logger.debug { "Merging #{newval.qp} into #{qp} #{attr_md} #{oldval.qp}..." } unless newval.empty?
|
767
|
+
newval.each do |src|
|
768
|
+
# if the match target is in the current collection, then update the matched
|
769
|
+
# target from the source.
|
770
|
+
# otherwise, if there is no match or the match is a new reference created
|
771
|
+
# from the match, then add the match to the oldval collection.
|
772
|
+
if matches.has_key?(src) then
|
773
|
+
# the source match
|
774
|
+
tgt = matches[src]
|
775
|
+
if oldval.include?(tgt) then
|
776
|
+
tgt.merge_attributes(src)
|
777
|
+
else
|
778
|
+
adds << tgt
|
779
|
+
end
|
780
|
+
else
|
781
|
+
adds << src
|
782
|
+
end
|
783
|
+
end
|
784
|
+
# add the unmatched sources
|
785
|
+
logger.debug { "Adding #{qp} #{attr_md} unmatched #{adds.qp}..." } unless adds.empty?
|
786
|
+
adds.each do |ref|
|
787
|
+
# if there is an owner writer attribute, then add the ref to the attribute collection by
|
788
|
+
# delegating to the owner writer. otherwise, add the ref to the attribute collection directly.
|
789
|
+
owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : oldval << ref
|
790
|
+
end
|
791
|
+
elsif newval then
|
792
|
+
if oldval then
|
793
|
+
oldval.merge(newval)
|
794
|
+
else
|
795
|
+
# the target is either a new object created in the match or the source value
|
796
|
+
ref = matches.has_key?(newval) ? matches[newval] : newval
|
797
|
+
# if the target is a dependent, then set the dependent owner, which in turn will
|
798
|
+
# set the attribute to the dependent. otherwise, set attribute to the target.
|
799
|
+
logger.debug { "Setting #{qp} #{attr_md} to #{ref.qp}..." }
|
800
|
+
owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : send(attr_md.writer, ref)
|
801
|
+
end
|
802
|
+
end
|
803
|
+
end
|
804
|
+
|
805
|
+
def delegate_to_inverse_setter(attr_md, ref, writer)
|
806
|
+
logger.debug { "Setting #{qp} #{attr_md} by setting the #{ref.qp} inverse attribute #{attr_md.inverse}..." }
|
807
|
+
ref.send(writer, self)
|
808
|
+
end
|
809
|
+
|
810
|
+
# Sets an exclusive dependent attribute to the given dependent dep.
|
811
|
+
# If dep is not nil, then this method calls the dep inv_writer with argument self
|
812
|
+
# before calling the writer with argument dep.
|
813
|
+
def set_exclusive_dependent(dep, writer, inv_writer)
|
814
|
+
dep.send(inv_writer, self) if dep
|
815
|
+
send(writer, dep)
|
816
|
+
end
|
817
|
+
|
818
|
+
# Sets the inversible attribute with the given accessors and inverse to
|
819
|
+
# the given newval. The inverse of the attribute is a collection accessed by
|
820
|
+
# calling inverse on newval.
|
821
|
+
#
|
822
|
+
# For an attribute +owner+ with writer +setOwner+ and inverse reader +deps+,
|
823
|
+
# this is equivalent to the following:
|
824
|
+
# class Dependent
|
825
|
+
# def owner=(o)
|
826
|
+
# owner.deps.delete(owner) if owner
|
827
|
+
# setOwner(o)
|
828
|
+
# o.deps << self if o
|
829
|
+
# end
|
830
|
+
# end
|
831
|
+
#
|
832
|
+
# @param [Resource] new_ref the new attribute reference value
|
833
|
+
# @param [(Symbol, Symbol)] accessors the reader and writer to use in setting
|
834
|
+
# the attribute
|
835
|
+
# @param [Symbol] inverse the inverse collection attribute to which
|
836
|
+
# this domain object will be added
|
837
|
+
def add_to_inverse_collection(new_ref, accessors, inverse)
|
838
|
+
reader, writer = accessors
|
839
|
+
# the current inverse
|
840
|
+
old_ref = send(reader)
|
841
|
+
# no-op if no change
|
842
|
+
return new_ref if old_ref == new_ref
|
843
|
+
|
844
|
+
# delete self from the current inverse reference collection
|
845
|
+
if old_ref then
|
846
|
+
old_ref.suspend_lazy_loader do
|
847
|
+
oldcoll = old_ref.send(inverse)
|
848
|
+
oldcoll.delete(self) if oldcoll
|
849
|
+
end
|
850
|
+
end
|
851
|
+
|
852
|
+
# call the writer on this object
|
853
|
+
send(writer, new_ref)
|
854
|
+
# add self to the inverse collection
|
855
|
+
if new_ref then
|
856
|
+
new_ref.suspend_lazy_loader do
|
857
|
+
newcoll = new_ref.send(inverse)
|
858
|
+
newcoll << self
|
859
|
+
unless newcoll then
|
860
|
+
raise TypeError.new("Cannot create #{new_ref.qp} #{inverse} collection to hold #{self}")
|
861
|
+
end
|
862
|
+
if old_ref then
|
863
|
+
logger.debug { "Moved #{qp} from #{reader} #{old_ref.qp} #{inverse} to #{new_ref.qp}." }
|
864
|
+
else
|
865
|
+
logger.debug { "Added #{qp} to #{reader} #{new_ref.qp} #{inverse}." }
|
866
|
+
end
|
867
|
+
end
|
868
|
+
end
|
869
|
+
new_ref
|
870
|
+
end
|
871
|
+
|
872
|
+
# Sets the attribute with the given accessors and inverse_writer to the given newval.
|
873
|
+
#
|
874
|
+
# For an attribute +owner+ with writer +setOwner+ and inverse +dep+, this is equivalent
|
875
|
+
# to the following:
|
876
|
+
# class Dependent
|
877
|
+
# def owner=(o)
|
878
|
+
# owner.dep = nil if owner
|
879
|
+
# setOwner(o)
|
880
|
+
# o.dep = self if o
|
881
|
+
# end
|
882
|
+
# end
|
883
|
+
def set_inversible_noncollection_attribute(newval, accessors, inverse_writer)
|
884
|
+
reader, writer = accessors
|
885
|
+
# the previous value
|
886
|
+
oldval = suspend_lazy_loader { send(reader) }
|
887
|
+
# bail if no change
|
888
|
+
return newval if newval.equal?(oldval)
|
889
|
+
|
890
|
+
# clear the previous inverse
|
891
|
+
oldval.send(inverse_writer, nil) if oldval
|
892
|
+
# call the writer
|
893
|
+
send(writer, newval)
|
894
|
+
logger.debug { "Moved #{qp} from #{oldval.qp} to #{newval.qp}." } if oldval and newval
|
895
|
+
# call the inverse writer on self
|
896
|
+
newval.send(inverse_writer, self) if newval
|
897
|
+
newval
|
898
|
+
end
|
899
|
+
|
900
|
+
# Returns 0 if attribute is a Java primitive number,
|
901
|
+
# +false+ if attribute is a Java primitive boolean, nil otherwise.
|
902
|
+
def empty_value(attribute)
|
903
|
+
type = java_type(attribute)
|
904
|
+
return unless type and type.primitive?
|
905
|
+
type.name == 'boolean' ? false : 0
|
906
|
+
end
|
907
|
+
|
908
|
+
# Returns the Java type of the given attribute, or nil if attribute is not a Java property attribute.
|
909
|
+
def java_type(attribute)
|
910
|
+
attr_md = self.class.attribute_metadata(attribute)
|
911
|
+
attr_md.property_descriptor.property_type if JavaAttributeMetadata === attr_md
|
912
|
+
end
|
913
|
+
|
914
|
+
# Returns the object in others which uniquely matches this domain object on the given attributes,
|
915
|
+
# or nil if there is no unique match. This method returns nil if any attributes value is nil.
|
916
|
+
def match_unique_object_with_attributes(others, attributes)
|
917
|
+
vh = value_hash(attributes)
|
918
|
+
return if vh.empty? or vh.size < attributes.size
|
919
|
+
matches = match_attribute_values(others, vh)
|
920
|
+
matches.first if matches.size == 1
|
921
|
+
end
|
922
|
+
|
923
|
+
# Returns the domain objects in others whose class is the same as this object's class
|
924
|
+
# and whose attribute values equal those in the given attr_value_hash.
|
925
|
+
def match_attribute_values(others, attr_value_hash)
|
926
|
+
others.select do |other|
|
927
|
+
self.class === other and attr_value_hash.all? do |attr, value|
|
928
|
+
matches_attribute_value?(other, attr, value)
|
929
|
+
end
|
930
|
+
end
|
931
|
+
end
|
932
|
+
|
933
|
+
# Returns whether this Resource's attribute value matches the fetched other attribute.
|
934
|
+
# A domain attribute match is determined by {#match?}.
|
935
|
+
# A non-domain attribute match is determined by an equality comparison.
|
936
|
+
def matches_attribute_value?(other, attribute, value)
|
937
|
+
other_val = other.send(attribute)
|
938
|
+
if Resource === value then
|
939
|
+
value.match?(other_val)
|
940
|
+
else
|
941
|
+
value == other_val
|
942
|
+
end
|
943
|
+
end
|
944
|
+
|
945
|
+
# Returns the attribute => value hash to use for matching this domain object as follows:
|
946
|
+
# * If this domain object has a database identifier, then the identifier is the sole match criterion attribute.
|
947
|
+
# * Otherwise, if a secondary key is defined for the object's class, then those attributes are used.
|
948
|
+
# * Otherwise, all attributes are used.
|
949
|
+
#
|
950
|
+
# If any secondary key value is nil, then this method returns an empty hash, since the search is ambiguous.
|
951
|
+
def search_attribute_values
|
952
|
+
# if this object has a database identifier, then the identifier is the search criterion
|
953
|
+
identifier.nil? ? non_id_search_attribute_values : { :identifier => identifier }
|
954
|
+
end
|
955
|
+
|
956
|
+
# Returns the attribute => value hash to use for matching this domain object.
|
957
|
+
# @see #search_attribute_values the method specification
|
958
|
+
def non_id_search_attribute_values
|
959
|
+
# if there is a secondary key, then search on those attributes.
|
960
|
+
# otherwise, search on all attributes.
|
961
|
+
key_attrs = self.class.secondary_key_attributes
|
962
|
+
attrs = key_attrs.empty? ? self.class.nondomain_java_attributes : key_attrs
|
963
|
+
# associate the values
|
964
|
+
attr_values = attrs.to_compact_hash { |attr| send(attr) }
|
965
|
+
# if there is no secondary key, then cull empty values
|
966
|
+
key_attrs.empty? ? attr_values.delete_if { |attr, value| value.nil? } : attr_values
|
967
|
+
end
|
968
|
+
end
|
969
|
+
end
|