caruby-core 1.5.5 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -0
- data/History.md +5 -1
- data/lib/caruby.rb +3 -5
- data/lib/caruby/caruby-src.tar.gz +0 -0
- data/lib/caruby/database.rb +53 -69
- data/lib/caruby/database/application_service.rb +25 -0
- data/lib/caruby/database/cache.rb +60 -0
- data/lib/caruby/database/fetched_matcher.rb +52 -38
- data/lib/caruby/database/lazy_loader.rb +4 -4
- data/lib/caruby/database/operation.rb +34 -0
- data/lib/caruby/database/persistable.rb +171 -86
- data/lib/caruby/database/persistence_service.rb +32 -34
- data/lib/caruby/database/persistifier.rb +100 -43
- data/lib/caruby/database/reader.rb +107 -85
- data/lib/caruby/database/reader_template_builder.rb +60 -0
- data/lib/caruby/database/saved_matcher.rb +3 -3
- data/lib/caruby/database/sql_executor.rb +88 -17
- data/lib/caruby/database/writer.rb +213 -177
- data/lib/caruby/database/writer_template_builder.rb +334 -0
- data/lib/caruby/{util → helpers}/controlled_value.rb +0 -0
- data/lib/caruby/{util → helpers}/coordinate.rb +4 -4
- data/lib/caruby/{util → helpers}/person.rb +3 -3
- data/lib/caruby/{util → helpers}/properties.rb +7 -9
- data/lib/caruby/{util → helpers}/roman.rb +2 -2
- data/lib/caruby/{util → helpers}/version.rb +1 -1
- data/lib/caruby/json/deserializer.rb +2 -2
- data/lib/caruby/json/serializer.rb +49 -7
- data/lib/caruby/metadata.rb +30 -0
- data/lib/caruby/metadata/java_property.rb +21 -0
- data/lib/caruby/metadata/propertied.rb +191 -0
- data/lib/caruby/metadata/property.rb +22 -0
- data/lib/caruby/metadata/property_characteristics.rb +201 -0
- data/lib/caruby/migration/migratable.rb +11 -182
- data/lib/caruby/rdbi/driver/jdbc.rb +446 -0
- data/lib/caruby/resource.rb +20 -823
- data/lib/caruby/version.rb +1 -1
- data/test/lib/caruby/database/cache_test.rb +54 -0
- data/test/lib/caruby/{util → helpers}/controlled_value_test.rb +3 -5
- data/test/lib/caruby/{util → helpers}/person_test.rb +4 -6
- data/test/lib/caruby/helpers/properties_test.rb +34 -0
- data/test/lib/caruby/{util → helpers}/roman_test.rb +2 -3
- data/test/lib/caruby/{util → helpers}/version_test.rb +2 -3
- data/test/lib/helper.rb +7 -0
- metadata +161 -214
- data/lib/caruby/cli/application.rb +0 -36
- data/lib/caruby/cli/command.rb +0 -202
- data/lib/caruby/csv/csv_mapper.rb +0 -159
- data/lib/caruby/csv/csvio.rb +0 -203
- data/lib/caruby/database/search_template_builder.rb +0 -56
- data/lib/caruby/database/store_template_builder.rb +0 -278
- data/lib/caruby/domain.rb +0 -193
- data/lib/caruby/domain/attribute.rb +0 -584
- data/lib/caruby/domain/attributes.rb +0 -628
- data/lib/caruby/domain/dependency.rb +0 -225
- data/lib/caruby/domain/id_alias.rb +0 -22
- data/lib/caruby/domain/importer.rb +0 -183
- data/lib/caruby/domain/introspection.rb +0 -176
- data/lib/caruby/domain/inverse.rb +0 -172
- data/lib/caruby/domain/inversible.rb +0 -90
- data/lib/caruby/domain/java_attribute.rb +0 -173
- data/lib/caruby/domain/merge.rb +0 -185
- data/lib/caruby/domain/metadata.rb +0 -142
- data/lib/caruby/domain/mixin.rb +0 -35
- data/lib/caruby/domain/properties.rb +0 -95
- data/lib/caruby/domain/reference_visitor.rb +0 -428
- data/lib/caruby/domain/uniquify.rb +0 -50
- data/lib/caruby/import/java.rb +0 -387
- data/lib/caruby/migration/migrator.rb +0 -918
- data/lib/caruby/migration/resource_module.rb +0 -9
- data/lib/caruby/migration/uniquify.rb +0 -17
- data/lib/caruby/util/attribute_path.rb +0 -44
- data/lib/caruby/util/cache.rb +0 -56
- data/lib/caruby/util/class.rb +0 -149
- data/lib/caruby/util/collection.rb +0 -1152
- data/lib/caruby/util/domain_extent.rb +0 -46
- data/lib/caruby/util/file_separator.rb +0 -65
- data/lib/caruby/util/inflector.rb +0 -27
- data/lib/caruby/util/log.rb +0 -95
- data/lib/caruby/util/math.rb +0 -12
- data/lib/caruby/util/merge.rb +0 -59
- data/lib/caruby/util/module.rb +0 -18
- data/lib/caruby/util/options.rb +0 -97
- data/lib/caruby/util/partial_order.rb +0 -35
- data/lib/caruby/util/pretty_print.rb +0 -204
- data/lib/caruby/util/stopwatch.rb +0 -74
- data/lib/caruby/util/topological_sync_enumerator.rb +0 -62
- data/lib/caruby/util/transitive_closure.rb +0 -55
- data/lib/caruby/util/tree.rb +0 -48
- data/lib/caruby/util/trie.rb +0 -37
- data/lib/caruby/util/uniquifier.rb +0 -30
- data/lib/caruby/util/validation.rb +0 -20
- data/lib/caruby/util/visitor.rb +0 -365
- data/lib/caruby/util/weak_hash.rb +0 -36
- data/test/lib/caruby/csv/csv_mapper_test.rb +0 -40
- data/test/lib/caruby/csv/csvio_test.rb +0 -69
- data/test/lib/caruby/database/persistable_test.rb +0 -92
- data/test/lib/caruby/domain/domain_test.rb +0 -112
- data/test/lib/caruby/domain/inversible_test.rb +0 -99
- data/test/lib/caruby/domain/reference_visitor_test.rb +0 -130
- data/test/lib/caruby/import/java_test.rb +0 -80
- data/test/lib/caruby/import/mixed_case_test.rb +0 -14
- data/test/lib/caruby/migration/test_case.rb +0 -102
- data/test/lib/caruby/test_case.rb +0 -230
- data/test/lib/caruby/util/cache_test.rb +0 -23
- data/test/lib/caruby/util/class_test.rb +0 -61
- data/test/lib/caruby/util/collection_test.rb +0 -398
- data/test/lib/caruby/util/command_test.rb +0 -55
- data/test/lib/caruby/util/domain_extent_test.rb +0 -60
- data/test/lib/caruby/util/file_separator_test.rb +0 -30
- data/test/lib/caruby/util/inflector_test.rb +0 -12
- data/test/lib/caruby/util/lazy_hash_test.rb +0 -34
- data/test/lib/caruby/util/merge_test.rb +0 -83
- data/test/lib/caruby/util/module_test.rb +0 -25
- data/test/lib/caruby/util/options_test.rb +0 -59
- data/test/lib/caruby/util/partial_order_test.rb +0 -42
- data/test/lib/caruby/util/pretty_print_test.rb +0 -85
- data/test/lib/caruby/util/properties_test.rb +0 -50
- data/test/lib/caruby/util/stopwatch_test.rb +0 -18
- data/test/lib/caruby/util/topological_sync_enumerator_test.rb +0 -69
- data/test/lib/caruby/util/transitive_closure_test.rb +0 -67
- data/test/lib/caruby/util/tree_test.rb +0 -23
- data/test/lib/caruby/util/trie_test.rb +0 -14
- data/test/lib/caruby/util/visitor_test.rb +0 -278
- data/test/lib/caruby/util/weak_hash_test.rb +0 -45
- data/test/lib/examples/clinical_trials/migration/migration_test.rb +0 -58
- data/test/lib/examples/clinical_trials/migration/test_case.rb +0 -38
data/lib/caruby/resource.rb
CHANGED
@@ -1,828 +1,25 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
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'
|
8
|
-
require 'caruby/domain/mixin'
|
9
|
-
require 'caruby/domain/merge'
|
10
|
-
require 'caruby/json/serializer'
|
11
|
-
require 'caruby/domain/reference_visitor'
|
12
|
-
require 'caruby/database/persistable'
|
13
|
-
require 'caruby/domain/inversible'
|
14
|
-
require 'caruby/domain/metadata'
|
15
|
-
require 'caruby/domain/mixin'
|
1
|
+
require 'jinx/resource'
|
2
|
+
require 'jinx/json/serializer'
|
16
3
|
require 'caruby/migration/migratable'
|
4
|
+
require 'caruby/database/persistable'
|
17
5
|
|
18
6
|
module CaRuby
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
7
|
+
# Augments +Jinx::Resource+ to inject {Propertied} persistence into introspected classes.
|
8
|
+
# A CaRuby application domain module includes +CaRuby::Resource+ and extends +CaRuby::Importer+.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # The application domain module.
|
12
|
+
# module Domain
|
13
|
+
# # Add persistence to the domain instances.
|
14
|
+
# include CaRuby::Resource
|
15
|
+
# # Add introspection to this domain module.
|
16
|
+
# extend Jinx::Importer
|
17
|
+
# # Add persistence to the domain classes.
|
18
|
+
# @metadata_module = CaRuby::Metadata
|
19
|
+
# end
|
22
20
|
module Resource
|
23
|
-
include
|
24
|
-
|
25
|
-
# @quirk JRuby Bug #5090 - JRuby 1.5 object_id is no longer a reserved method, and results
|
26
|
-
# in a String value rather than an Integer (cf. http://jira.codehaus.org/browse/JRUBY-5090).
|
27
|
-
# Work-around is to make a proxy object id.
|
28
|
-
#
|
29
|
-
# @return [Integer] the object id
|
30
|
-
def proxy_object_id
|
31
|
-
# make a hash code on demand
|
32
|
-
@_hc ||= (Object.new.object_id * 31) + 17
|
33
|
-
end
|
34
|
-
|
35
|
-
# Prints this object's class demodulized name and object id.
|
36
|
-
def print_class_and_id
|
37
|
-
"#{self.class.qp}@#{proxy_object_id}"
|
38
|
-
end
|
39
|
-
|
40
|
-
alias :qp :print_class_and_id
|
41
|
-
|
42
|
-
# Sets the default attribute values for this domain object and its dependents. If this Resource
|
43
|
-
# does not have an identifier, then missing attributes are set to the values defined by
|
44
|
-
# {Domain::Attributes#add_attribute_defaults}.
|
45
|
-
#
|
46
|
-
# Subclasses should override the private {#add_defaults_local} method rather than this method.
|
47
|
-
#
|
48
|
-
# @return [Resource] self
|
49
|
-
def add_defaults
|
50
|
-
# If there is an owner, then delegate to the owner.
|
51
|
-
# Otherwise, add defaults to this object.
|
52
|
-
par = owner
|
53
|
-
if par and par.identifier.nil? then
|
54
|
-
par.add_defaults
|
55
|
-
else
|
56
|
-
logger.debug { "Adding defaults to #{qp} and its dependents..." }
|
57
|
-
# apply the local and dependent defaults
|
58
|
-
add_defaults_recursive
|
59
|
-
end
|
60
|
-
self
|
61
|
-
end
|
62
|
-
|
63
|
-
# Sets the default attribute values for this auto-generated domain object.
|
64
|
-
def add_defaults_autogenerated
|
65
|
-
add_defaults_recursive
|
66
|
-
end
|
67
|
-
|
68
|
-
# Validates this domain object and its #{Domain::Attributes#unproxied_savable_template_attributes}
|
69
|
-
# for completeness prior to a database create operation.
|
70
|
-
# An object without an identifer is valid if it contains a non-nil value for each mandatory property.
|
71
|
-
# Objects which have an identifier or have already been validated are skipped.
|
72
|
-
#
|
73
|
-
# Subclasses should not override this method, but override the private {#validate_local} instead.
|
74
|
-
#
|
75
|
-
# @return [Resource] this domain object
|
76
|
-
# @raise (see #validate_local)
|
77
|
-
def validate
|
78
|
-
if identifier.nil? and not @validated then
|
79
|
-
validate_local
|
80
|
-
@validated = true
|
81
|
-
end
|
82
|
-
self.class.unproxied_savable_template_attributes.each do |attr|
|
83
|
-
send(attr).enumerate { |dep| dep.validate }
|
84
|
-
end
|
85
|
-
self
|
86
|
-
end
|
87
|
-
|
88
|
-
# Adds the default values to this object, if it is not already fetched, and its dependents.
|
89
|
-
#
|
90
|
-
# This method is intended for use only by the {#add_defaults} method.
|
91
|
-
def add_defaults_recursive
|
92
|
-
# Add the local defaults.
|
93
|
-
# The lazy loader is enabled in order to allow subclass add_defaults_local implementations
|
94
|
-
# to pick up load-on-demand references used to set defaults.
|
95
|
-
database.lazy_loader.enable { add_defaults_local }
|
96
|
-
# add dependent defaults
|
97
|
-
each_defaults_dependent { |dep| dep.add_defaults_recursive }
|
98
|
-
end
|
99
|
-
|
100
|
-
# @return [Boolean] whether this domain object has {#searchable_attributes}
|
101
|
-
def searchable?
|
102
|
-
not searchable_attributes.nil?
|
103
|
-
end
|
104
|
-
|
105
|
-
# Returns the attributes to use for a search using this domain object as a template, determined
|
106
|
-
# as follows:
|
107
|
-
# * If this domain object has a non-nil primary key, then the primary key is the search criterion.
|
108
|
-
# * Otherwise, if this domain object has a secondary key and each key attribute value is not nil,
|
109
|
-
# then the secondary key is the search criterion.
|
110
|
-
# * Otherwise, if this domain object has an alternate key and each key attribute value is not nil,
|
111
|
-
# then the aklternate key is the search criterion.
|
112
|
-
#
|
113
|
-
# @return [<Symbol>] the attributes to use for a search on this domain object
|
114
|
-
def searchable_attributes
|
115
|
-
key_attrs = self.class.primary_key_attributes
|
116
|
-
return key_attrs if key_searchable?(key_attrs)
|
117
|
-
key_attrs = self.class.secondary_key_attributes
|
118
|
-
return key_attrs if key_searchable?(key_attrs)
|
119
|
-
key_attrs = self.class.alternate_key_attributes
|
120
|
-
return key_attrs if key_searchable?(key_attrs)
|
121
|
-
end
|
122
|
-
|
123
|
-
# Returns a new domain object with the given attributes copied from this domain object.
|
124
|
-
# The attributes argument consists of either attribute Symbols or a single Enumerable
|
125
|
-
# consisting of Symbols.
|
126
|
-
# The default attributes are the {Domain::Attributes#nondomain_attributes}.
|
127
|
-
#
|
128
|
-
# @param [<Symbol>, (<Symbol>)] attributes the attributes to copy
|
129
|
-
# @return [Resource] a copy of this domain object
|
130
|
-
def copy(*attributes)
|
131
|
-
if attributes.empty? then
|
132
|
-
attributes = self.class.nondomain_attributes
|
133
|
-
elsif Enumerable === attributes.first then
|
134
|
-
raise ArgumentError.new("#{qp} copy attributes argument is not a Symbol: #{attributes.first}") unless attributes.size == 1
|
135
|
-
attributes = attributes.first
|
136
|
-
end
|
137
|
-
self.class.new.merge_attributes(self, attributes)
|
138
|
-
end
|
139
|
-
|
140
|
-
# Clears the given attribute value. If the current value responds to the +clear+ method,
|
141
|
-
# then the current value is cleared. Otherwise, the value is set to {Domain::Metadata#empty_value}.
|
142
|
-
#
|
143
|
-
# @param [Symbol] attribute the attribute to clear
|
144
|
-
def clear_attribute(attribute)
|
145
|
-
# the current value to clear
|
146
|
-
current = send(attribute)
|
147
|
-
return if current.nil?
|
148
|
-
# call the current value clear if possible.
|
149
|
-
# otherwise, set the attribute to the empty value.
|
150
|
-
if current.respond_to?(:clear) then
|
151
|
-
current.clear
|
152
|
-
else
|
153
|
-
writer = self.class.attribute_metadata(attribute).writer
|
154
|
-
value = self.class.empty_value(attribute)
|
155
|
-
send(writer, value)
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
# Sets this domain object's attribute to the value. This method clears the current attribute value,
|
160
|
-
# if any, and merges the new value. Merge rather than assignment ensures that a collection type
|
161
|
-
# is preserved, e.g. an Array value is assigned to a set domain type by first clearing the set
|
162
|
-
# and then merging the array content into the set.
|
163
|
-
#
|
164
|
-
# @see Mergeable#merge_attribute
|
165
|
-
def set_attribute(attribute, value)
|
166
|
-
# bail out if the value argument is the current value
|
167
|
-
return value if value.equal?(send(attribute))
|
168
|
-
clear_attribute(attribute)
|
169
|
-
merge_attribute(attribute, value)
|
170
|
-
end
|
171
|
-
|
172
|
-
# Returns the secondary key attribute values as follows:
|
173
|
-
# * If there is no secondary key, then this method returns nil.
|
174
|
-
# * Otherwise, if the secondary key attributes is a singleton Array, then the key is the
|
175
|
-
# value of the sole key attribute.
|
176
|
-
# * Otherwise, the key is an Array of the key attribute values.
|
177
|
-
#
|
178
|
-
# @return [Array, Object] the key attribute values
|
179
|
-
def key
|
180
|
-
attrs = self.class.secondary_key_attributes
|
181
|
-
case attrs.size
|
182
|
-
when 0 then nil
|
183
|
-
when 1 then send(attrs.first)
|
184
|
-
else attrs.map { |attr| send(attr) }
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
# @return [Resource, nil] the domain object that owns this object, or nil if this object
|
189
|
-
# is not dependent on an owner
|
190
|
-
def owner
|
191
|
-
self.class.owner_attributes.detect_value { |attr| send(attr) }
|
192
|
-
end
|
193
|
-
|
194
|
-
# @return [Symbol, nil] the attribute for which there is an owner reference,
|
195
|
-
# or nil if this domain object does not reference an owner
|
196
|
-
def effective_owner_attribute
|
197
|
-
self.class.owner_attributes.detect { |attr| send(attr) }
|
198
|
-
end
|
199
|
-
|
200
|
-
# Sets this dependent's owner attribute to the given domain object.
|
201
|
-
#
|
202
|
-
# @param [Resource] owner the owner domain object
|
203
|
-
# @raise [NoMethodError] if this Resource's class does not have exactly one owner attribute
|
204
|
-
def owner=(owner)
|
205
|
-
attr = self.class.owner_attribute
|
206
|
-
if attr.nil? then raise NoMethodError.new("#{self.class.qp} does not have a unique owner attribute") end
|
207
|
-
set_attribute(attr, owner)
|
208
|
-
end
|
209
|
-
|
210
|
-
# @param [Resource] other the domain object to check
|
211
|
-
# @return [Boolean] whether the other domain object is this object's {#owner} or an
|
212
|
-
# {#owner_ancestor?} of this object's {#owner}
|
213
|
-
def owner_ancestor?(other)
|
214
|
-
owner = self.owner
|
215
|
-
owner and (owner == other or owner.owner_ancestor?(other))
|
216
|
-
end
|
217
|
-
|
218
|
-
# Returns an attribute => value hash for the specified attributes with a non-nil, non-empty value.
|
219
|
-
# The default attributes are this domain object's class {Domain::Attributes#attributes}.
|
220
|
-
# Only non-nil attributes defined by this Resource are included in the result hash.
|
221
|
-
#
|
222
|
-
# @param [<Symbol>, nil] attributes the attributes to merge
|
223
|
-
# @return [{Symbol => Object}] the attribute => value hash
|
224
|
-
def value_hash(attributes=nil)
|
225
|
-
attributes ||= self.class.attributes
|
226
|
-
attributes.to_compact_hash { |attr| send(attr) if self.class.method_defined?(attr) }
|
227
|
-
end
|
228
|
-
|
229
|
-
# Returns the domain object references for the given attributes.
|
230
|
-
#
|
231
|
-
# @param [<Symbol>, nil] the domain attributes to include, or nil to include all domain attributes
|
232
|
-
# @return [<Resource>] the referenced attribute domain object values
|
233
|
-
def references(attributes=nil)
|
234
|
-
attributes ||= self.class.domain_attributes
|
235
|
-
attributes.map { |attr| send(attr) }.flatten.compact
|
236
|
-
end
|
237
|
-
|
238
|
-
# @return [Boolean] whether this domain object is dependent on another entity
|
239
|
-
def dependent?
|
240
|
-
self.class.dependent?
|
241
|
-
end
|
242
|
-
|
243
|
-
# @return [Boolean] whether this domain object is not dependent on another entity
|
244
|
-
def independent?
|
245
|
-
not dependent?
|
246
|
-
end
|
247
|
-
|
248
|
-
# Enumerates over this domain object's dependents.
|
249
|
-
#
|
250
|
-
# @yield [dep] the block to execute on the dependent
|
251
|
-
# @yieldparam [Resource] dep the dependent
|
252
|
-
def each_dependent
|
253
|
-
self.class.dependent_attributes.each do |attr|
|
254
|
-
send(attr).enumerate { |dep| yield dep }
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
# @return [Enumerable] this domain object's dependents
|
259
|
-
def dependents
|
260
|
-
enum_for(:each_dependent)
|
261
|
-
end
|
262
|
-
|
263
|
-
# Returns the attributes which are required for save. This base implementation returns the
|
264
|
-
# class {Domain::Attributes#mandatory_attributes}. Subclasses can override this method
|
265
|
-
# for domain object state-specific refinements.
|
266
|
-
#
|
267
|
-
# @return [<Symbol>] the required attributes for a save operation
|
268
|
-
def mandatory_attributes
|
269
|
-
self.class.mandatory_attributes
|
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
|
-
#
|
284
|
-
# @param [Symbol] attribute the dependent attribute
|
285
|
-
# @return [<Resource>] the attribute value, wrapped in an array if necessary
|
286
|
-
def direct_dependents(attribute)
|
287
|
-
deps = send(attribute)
|
288
|
-
case deps
|
289
|
-
when Enumerable then deps
|
290
|
-
when nil then Array::EMPTY_ARRAY
|
291
|
-
else [deps]
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
# @param [Resource] the domain object to match
|
296
|
-
# @return [Boolean] whether this object matches the fetched other object on class
|
297
|
-
# and key values
|
298
|
-
def match?(other)
|
299
|
-
match_in([other])
|
300
|
-
end
|
301
|
-
|
302
|
-
# Matches this dependent domain object with the others on type and key attributes
|
303
|
-
# in the scope of a parent object.
|
304
|
-
# Returns the object in others which matches this domain object, or nil if none.
|
305
|
-
#
|
306
|
-
# The match attributes are, in order:
|
307
|
-
# * the primary key
|
308
|
-
# * the secondary key
|
309
|
-
# * the alternate key
|
310
|
-
#
|
311
|
-
# This domain object is matched against the others on the above attributes in succession
|
312
|
-
# until a unique match is found. The key attribute matches are strict, i.e. each
|
313
|
-
# key attribute value must be non-nil and match the other value.
|
314
|
-
#
|
315
|
-
# @param [<Resource>] the candidate domain object matches
|
316
|
-
# @return [Resource, nil] the matching domain object, or nil if no match
|
317
|
-
def match_in(others)
|
318
|
-
# trivial case: self is in others
|
319
|
-
return self if others.include?(self)
|
320
|
-
# filter for the same type
|
321
|
-
others = others.filter { |other| self.class === other }
|
322
|
-
# match on primary, secondary or alternate key
|
323
|
-
match_unique_object_with_attributes(others, self.class.primary_key_attributes) or
|
324
|
-
match_unique_object_with_attributes(others, self.class.secondary_key_attributes) or
|
325
|
-
match_unique_object_with_attributes(others, self.class.alternate_key_attributes)
|
326
|
-
end
|
327
|
-
|
328
|
-
# Returns the match of this domain object in the scope of a matching owner as follows:
|
329
|
-
# * If {#match_in} returns a match, then that match is the result is used.
|
330
|
-
# * Otherwise, if this is a dependent attribute then the match is attempted on a
|
331
|
-
# secondary key without owner attributes. Defaults are added to this object in order
|
332
|
-
# to pick up potential secondary key values.
|
333
|
-
#
|
334
|
-
# @param (see #match_in)
|
335
|
-
# @return (see #match_in)
|
336
|
-
def match_in_owner_scope(others)
|
337
|
-
match_in(others) or others.detect { |other| match_without_owner_attribute?(other) }
|
338
|
-
end
|
339
|
-
|
340
|
-
# @return [{Resouce => Resource}] a source => target hash of the given sources which match
|
341
|
-
# the targets using the {#match_in} method
|
342
|
-
def self.match_all(sources, targets)
|
343
|
-
DEF_MATCHER.match(sources, targets)
|
344
|
-
end
|
345
|
-
|
346
|
-
# Returns the difference between this Persistable and the other Persistable for the
|
347
|
-
# given attributes. The default attributes are the {Domain::Attributes#nondomain_attributes}.
|
348
|
-
#
|
349
|
-
# @param [Resource] other the domain object to compare
|
350
|
-
# @param [<Symbol>, nil] attributes the attributes to compare
|
351
|
-
# @return (see Hashable#diff)
|
352
|
-
def diff(other, attributes=nil)
|
353
|
-
attributes ||= self.class.nondomain_attributes
|
354
|
-
vh = value_hash(attributes)
|
355
|
-
ovh = other.value_hash(attributes)
|
356
|
-
vh.diff(ovh) { |key, v1, v2| Resource.value_equal?(v1, v2) }
|
357
|
-
end
|
358
|
-
|
359
|
-
# Returns the domain object in others which matches this dependent domain object
|
360
|
-
# within the scope of a parent on a minimally acceptable constraint. This method
|
361
|
-
# is used when this object might be partially complete--say, lacking a secondary key
|
362
|
-
# value--but is expected to match one of the others, e.g. when matching a referenced
|
363
|
-
# object to its fetched counterpart.
|
364
|
-
#
|
365
|
-
# This base implementation returns whether the following conditions hold:
|
366
|
-
# 1. other is the same class as this domain object
|
367
|
-
# 2. if both identifiers are non-nil, then they are equal
|
368
|
-
#
|
369
|
-
# Subclasses can override this method to impose additional minimal consistency constraints.
|
370
|
-
#
|
371
|
-
# @param [Resource] other the domain object to match against
|
372
|
-
# @return [Boolean] whether this Resource equals other
|
373
|
-
def minimal_match?(other)
|
374
|
-
self.class === other and
|
375
|
-
(identifier.nil? or other.identifier.nil? or identifier == other.identifier)
|
376
|
-
end
|
377
|
-
|
378
|
-
# Returns an enumerator on the transitive closure of the reference attributes.
|
379
|
-
# If a block is given to this method, then the block called on each reference determines
|
380
|
-
# which attributes to visit. Otherwise, all saved references are visited.
|
381
|
-
#
|
382
|
-
# @yield [ref] reference visit attribute selector
|
383
|
-
# @yieldparam [Resource] ref the domain object to visit
|
384
|
-
# @return [Enumerable] the reference transitive closure
|
385
|
-
def reference_hierarchy
|
386
|
-
ReferenceVisitor.new { |ref| yield ref }.to_enum(self)
|
387
|
-
end
|
388
|
-
|
389
|
-
# Returns the value for the given attribute path Array or String expression, e.g.:
|
390
|
-
# study.path_value("site.address.state")
|
391
|
-
# follows the +study+ -> +site+ -> +address+ -> +state+ accessors and returns the +state+
|
392
|
-
# value, or nil if any intermediate reference is nil.
|
393
|
-
# The array form for the above example is:
|
394
|
-
# study.path_value([:site, :address, :state])
|
395
|
-
#
|
396
|
-
# @param [<Symbol>] path the attributes to navigate
|
397
|
-
# @return the attribute navigation result
|
398
|
-
def path_value(path)
|
399
|
-
path = path.split('.').map { |attr| attr.to_sym } if String === path
|
400
|
-
path.inject(self) do |parent, attr|
|
401
|
-
value = parent.send(attr)
|
402
|
-
return if value.nil?
|
403
|
-
value
|
404
|
-
end
|
405
|
-
end
|
406
|
-
|
407
|
-
# Applies the operator block to this object and each domain object in the reference path.
|
408
|
-
# This method visits the transitive closure of each recursive path attribute.
|
409
|
-
#
|
410
|
-
# For example, given the attributes:
|
411
|
-
# treatment: BioMaterial -> Treatment
|
412
|
-
# measurement: Treatment -> BioMaterial
|
413
|
-
# and +BioMaterial+ instance +biospecimen+, then:
|
414
|
-
# biospecimen.visit_path[:treatment, :measurement, :biomaterial]
|
415
|
-
# visits +biospecimen+ and all biomaterial, treatments and measurements derived
|
416
|
-
# directly or indirectly from +biospecimen+.
|
417
|
-
#
|
418
|
-
# @param [<Symbol>] path the attributes to visit
|
419
|
-
# @yieldparam [Symbol] attribute the attribute to visit
|
420
|
-
# @return the visit result
|
421
|
-
def visit_path(path, &operator)
|
422
|
-
visitor = ReferencePathVisitorFactory.create(self.class, path)
|
423
|
-
visitor.visit(self, &operator)
|
424
|
-
end
|
425
|
-
|
426
|
-
# Applies the operator block to the transitive closure of this domain object's dependency relation.
|
427
|
-
# The block argument is a dependent.
|
428
|
-
#
|
429
|
-
# @yield [dep] operation on the visited domain object
|
430
|
-
# @yieldparam [Resource] dep the domain object to visit
|
431
|
-
def visit_dependents(&operator) # :yields: dependent
|
432
|
-
DEPENDENT_VISITOR.visit(self, &operator)
|
433
|
-
end
|
434
|
-
|
435
|
-
# Applies the operator block to the transitive closure of this domain object's owner relation.
|
436
|
-
#
|
437
|
-
# @yield [dep] operation on the visited domain object
|
438
|
-
# @yieldparam [Resource] dep the domain object to visit
|
439
|
-
def visit_owners(&operator) # :yields: owner
|
440
|
-
ref = owner
|
441
|
-
yield(ref) and ref.visit_owners(&operator) if ref
|
442
|
-
end
|
443
|
-
|
444
|
-
# @param q the PrettyPrint queue
|
445
|
-
# @return [String] the formatted content of this Resource
|
446
|
-
def pretty_print(q)
|
447
|
-
q.text(qp)
|
448
|
-
content = printable_content
|
449
|
-
q.pp_hash(content) unless content.empty?
|
450
|
-
end
|
451
|
-
|
452
|
-
# Prints this domain object's content and recursively prints the referenced content.
|
453
|
-
# The optional selector block determines the attributes to print. The default is the
|
454
|
-
# {Domain::Attributes#java_attributes}. The database lazy loader is disabled during
|
455
|
-
# the execution of this method. Thus, the printed content reflects the transient
|
456
|
-
# in-memory object graph rather than the persistent content.
|
457
|
-
#
|
458
|
-
# @yield [owner] the owner attribute selector
|
459
|
-
# @yieldparam [Resource] owner the domain object to print
|
460
|
-
# @return [String] the domain object content
|
461
|
-
def dump(&selector)
|
462
|
-
do_without_lazy_loader { DetailPrinter.new(self, &selector).pp_s }
|
463
|
-
end
|
464
|
-
|
465
|
-
# Prints this domain object in the format:
|
466
|
-
# class_name@object_id{attribute => value ...}
|
467
|
-
# The default attributes include identifying attributes.
|
468
|
-
#
|
469
|
-
# @param [<Symbol>] attributes the attributes to print
|
470
|
-
# @return [String] the formatted content
|
471
|
-
def to_s(attributes=nil)
|
472
|
-
content = printable_content(attributes)
|
473
|
-
content_s = content.pp_s(:single_line) unless content.empty?
|
474
|
-
"#{print_class_and_id}#{content_s}"
|
475
|
-
end
|
476
|
-
|
477
|
-
alias :inspect :to_s
|
478
|
-
|
479
|
-
# Returns this domain object's attributes content as an attribute => value hash
|
480
|
-
# suitable for printing.
|
481
|
-
#
|
482
|
-
# The default attributes are this object's saved attributes. The optional
|
483
|
-
# reference_printer is used to print a referenced domain object.
|
484
|
-
#
|
485
|
-
# @param [<Symbol>, nil] attributes the attributes to print
|
486
|
-
# @yield [ref] the reference print formatter
|
487
|
-
# @yieldparam [Rresource] ref the referenced domain object to print
|
488
|
-
# @return [{Symbol => String}] the attribute => content hash
|
489
|
-
def printable_content(attributes=nil, &reference_printer) # :yields: reference
|
490
|
-
attributes ||= printworthy_attributes
|
491
|
-
vh = value_hash(attributes)
|
492
|
-
vh.transform { |value| printable_value(value, &reference_printer) }
|
493
|
-
end
|
494
|
-
|
495
|
-
# Returns whether value equals other modulo the given matches according to the following tests:
|
496
|
-
# * _value_ == _other_
|
497
|
-
# * _value_ and _other_ are Resource instances and _value_ is a {#match?} with _other_.
|
498
|
-
# * _value_ and _other_ are Enumerable with members equal according to the above conditions.
|
499
|
-
# * _value_ and _other_ are DateTime instances and are equal to within one second.
|
500
|
-
#
|
501
|
-
# The DateTime comparison accounts for differences in the Ruby -> Java -> Ruby roundtrip
|
502
|
-
# of a date attribute, which loses the seconds fraction.
|
503
|
-
#
|
504
|
-
# @return whether value and other are equal according to the above tests
|
505
|
-
def self.value_equal?(value, other, matches=nil)
|
506
|
-
if value == other then
|
507
|
-
true
|
508
|
-
elsif value.collection? and other.collection? then
|
509
|
-
collection_value_equal?(value, other, matches)
|
510
|
-
elsif DateTime === value and DateTime === other then
|
511
|
-
(value - other).abs.floor.zero?
|
512
|
-
elsif Resource === value and value.class === other then
|
513
|
-
value.match?(other)
|
514
|
-
elsif matches then
|
515
|
-
matches[value] == other
|
516
|
-
else
|
517
|
-
false
|
518
|
-
end
|
519
|
-
end
|
520
|
-
|
521
|
-
protected
|
522
|
-
|
523
|
-
# Returns the required attributes for this domain object which are nil or empty.
|
524
|
-
#
|
525
|
-
# This method is in protected scope to allow the +CaTissue+ domain module to
|
526
|
-
# work around a caTissue bug (see that module for details). Other definitions
|
527
|
-
# of this method are discouraged.
|
528
|
-
def missing_mandatory_attributes
|
529
|
-
mandatory_attributes.select { |attr| send(attr).nil_or_empty? }
|
530
|
-
end
|
531
|
-
|
532
|
-
private
|
533
|
-
|
534
|
-
# The copy merge call options.
|
535
|
-
COPY_MERGE_OPTS = {:inverse => false}
|
536
|
-
|
537
|
-
# The dependent attribute visitor.
|
538
|
-
#
|
539
|
-
# @see #visit_dependents
|
540
|
-
DEPENDENT_VISITOR = CaRuby::ReferenceVisitor.new { |obj| obj.class.dependent_attributes }
|
541
|
-
|
542
|
-
# Matches the given targets to sources using {Resource#match_in}.
|
543
|
-
class Matcher
|
544
|
-
def match(sources, targets)
|
545
|
-
unmatched = Set === sources ? sources.dup : sources.to_set
|
546
|
-
matches = {}
|
547
|
-
targets.each do |tgt|
|
548
|
-
src = tgt.match_in(unmatched)
|
549
|
-
if src then
|
550
|
-
unmatched.delete(src)
|
551
|
-
matches[src] = tgt
|
552
|
-
end
|
553
|
-
end
|
554
|
-
matches
|
555
|
-
end
|
556
|
-
end
|
557
|
-
|
558
|
-
DEF_MATCHER = Matcher.new
|
559
|
-
|
560
|
-
# Sets the default attribute values for this domain object. Unlike {#add_defaults}, this
|
561
|
-
# method does not set defaults for dependents. This method sets the configuration values
|
562
|
-
# for this domain object as described in {#add_defaults}, but does not set defaults for
|
563
|
-
# dependents.
|
564
|
-
#
|
565
|
-
# This method is the integration point for subclasses to augment defaults with programmatic logic.
|
566
|
-
# If a subclass overrides this method, then it should call super before setting the local
|
567
|
-
# default attributes. This ensures that configuration defaults takes precedence.
|
568
|
-
def add_defaults_local
|
569
|
-
logger.debug { "Adding defaults to #{qp}..." }
|
570
|
-
merge_attributes(self.class.defaults)
|
571
|
-
end
|
572
|
-
|
573
|
-
# Validates that this domain contains a non-nil value for each mandatory property.
|
574
|
-
#
|
575
|
-
# Subclasses can override this method for additional validation, but should call super first.
|
576
|
-
#
|
577
|
-
# @raise [ValidationError] if a mandatory attribute value is missing
|
578
|
-
def validate_local
|
579
|
-
logger.debug { "Validating #{qp} required attributes #{self.mandatory_attributes.to_a.to_series}..." }
|
580
|
-
invalid = missing_mandatory_attributes
|
581
|
-
unless invalid.empty? then
|
582
|
-
logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
|
583
|
-
raise ValidationError.new("Required attribute value missing for #{self}: #{invalid.join(', ')}")
|
584
|
-
end
|
585
|
-
if self.class.bidirectional_dependent? and not owner then
|
586
|
-
raise ValidationError.new("Dependent #{self} does not reference an owner")
|
587
|
-
end
|
588
|
-
end
|
589
|
-
|
590
|
-
# Enumerates the dependents for setting defaults. Subclasses can override if the
|
591
|
-
# dependents must be visited in a certain order.
|
592
|
-
alias :each_defaults_dependent :each_dependent
|
593
|
-
|
594
|
-
# @return [Boolean] whether the given key attributes is non-empty and each attribute in the key has a non-nil value
|
595
|
-
def key_searchable?(attributes)
|
596
|
-
not (attributes.empty? or attributes.any? { |attr| send(attr).nil? })
|
597
|
-
end
|
598
|
-
|
599
|
-
def self.collection_value_equal?(value, other, matches=nil)
|
600
|
-
value.size == other.size and value.all? { |v| other.include?(v) or (matches and other.include?(matches[v])) }
|
601
|
-
end
|
602
|
-
|
603
|
-
# A DetailPrinter formats a domain object value for printing using {#to_s} the first time the object
|
604
|
-
# is encountered and a ReferencePrinter on the object subsequently.
|
605
|
-
class DetailPrinter
|
606
|
-
alias :to_s :pp_s
|
607
|
-
|
608
|
-
alias :inspect :to_s
|
609
|
-
|
610
|
-
# Creates a DetailPrinter on the base object.
|
611
|
-
def initialize(base, visited=Set.new, &selector)
|
612
|
-
@base = base
|
613
|
-
@visited = visited << base
|
614
|
-
@selector = selector || Proc.new { |ref| ref.class.printable_attributes }
|
615
|
-
end
|
616
|
-
|
617
|
-
def pretty_print(q)
|
618
|
-
q.text(@base.qp)
|
619
|
-
# pretty-print the standard attribute values
|
620
|
-
attrs = @selector.call(@base)
|
621
|
-
content = @base.printable_content(attrs) do |ref|
|
622
|
-
@visited.include?(ref) ? ReferencePrinter.new(ref) : DetailPrinter.new(ref, @visited) { |ref| @selector.call(ref) }
|
623
|
-
end
|
624
|
-
q.pp_hash(content)
|
625
|
-
end
|
626
|
-
end
|
627
|
-
|
628
|
-
# A ReferencePrinter formats a reference domain object value for printing with just the class and Ruby object_id.
|
629
|
-
class ReferencePrinter
|
630
|
-
extend Forwardable
|
631
|
-
|
632
|
-
def_delegator(:@base, :qp, :to_s)
|
633
|
-
|
634
|
-
alias :inspect :to_s
|
635
|
-
|
636
|
-
# Creates a ReferencePrinter on the base object.
|
637
|
-
def initialize(base)
|
638
|
-
@base = base
|
639
|
-
end
|
640
|
-
end
|
641
|
-
|
642
|
-
# Returns a value suitable for printing. If value is a domain object, then the block provided to this method is called.
|
643
|
-
# The default block creates a new ReferencePrinter on the value.
|
644
|
-
def printable_value(value, &reference_printer)
|
645
|
-
Collector.on(value) do |item|
|
646
|
-
if Resource === item then
|
647
|
-
block_given? ? yield(item) : printable_value(item) { |ref| ReferencePrinter.new(ref) }
|
648
|
-
else
|
649
|
-
item
|
650
|
-
end
|
651
|
-
end
|
652
|
-
end
|
653
|
-
|
654
|
-
# Returns an attribute => value hash for the +identifier+ attribute, if there is a non_nil +identifier+,
|
655
|
-
# If +identifier+ is nil, then this method returns the secondary key attributes, if they exist,
|
656
|
-
# or the mergeable attributes otherwise. If this is a dependent object, then the owner attribute is
|
657
|
-
# removed from the returned array.
|
658
|
-
def printworthy_attributes
|
659
|
-
return self.class.primary_key_attributes if identifier
|
660
|
-
attrs = self.class.secondary_key_attributes
|
661
|
-
attrs = self.class.nondomain_java_attributes if attrs.empty?
|
662
|
-
attrs = self.class.fetched_attributes if attrs.empty?
|
663
|
-
attrs
|
664
|
-
end
|
665
|
-
|
666
|
-
# Substitutes attribute with the standard attribute and a Java non-Domain instance value with a Domain object if necessary.
|
667
|
-
#
|
668
|
-
# Returns the [standard attribute, standard value] array.
|
669
|
-
def standardize_attribute_value(attribute, value)
|
670
|
-
attr_md = self.class.attribute_metadata(attribute)
|
671
|
-
if attr_md.nil? then
|
672
|
-
raise ArgumentError.new("#{attribute} is neither a #{self.class.qp} standard attribute nor an alias for a standard attribute")
|
673
|
-
end
|
674
|
-
# standardize the value if necessary
|
675
|
-
std_val = attr_md.type && attr_md.type < Resource ? standardize_domain_value(value) : value
|
676
|
-
[attr_md.to_sym, std_val]
|
677
|
-
end
|
678
|
-
|
679
|
-
# Returns a Domain object for a Java non-Domain instance value.
|
680
|
-
def standardize_domain_value(value)
|
681
|
-
if value.nil? or Resource === value then
|
682
|
-
value
|
683
|
-
elsif Enumerable === value then
|
684
|
-
# value is a collection; if value is a nested collection (highly unlikely), then recursively standarize
|
685
|
-
# the value collection members. otherwise, leave the value alone.
|
686
|
-
value.empty? || Resource === value.first ? value : value.map { |item| standardize_domain_value(item) }
|
687
|
-
else
|
688
|
-
# return a new Domain object built from the source Java domain object
|
689
|
-
# (unlikely unless value is a weird toxic Hibernate proxy)
|
690
|
-
logger.debug { "Creating standard domain object from #{value}..." }
|
691
|
-
Domain.const_get(value.class.qp).new.merge_attributes(value)
|
692
|
-
end
|
693
|
-
end
|
694
|
-
|
695
|
-
# Returns whether the other domain object matches this domain object on a secondary
|
696
|
-
# key without owner attributes. Defaults are added to this object in order to pick up
|
697
|
-
# potential secondary key values.
|
698
|
-
#
|
699
|
-
# @param (see #match_in)
|
700
|
-
# @return [Boolean] whether the other domain object matches this domain object on a
|
701
|
-
# secondary key without owner attributes
|
702
|
-
def match_without_owner_attribute?(other)
|
703
|
-
return unless other.class == self.class
|
704
|
-
oattrs = self.class.owner_attributes
|
705
|
-
return if oattrs.empty?
|
706
|
-
# match on the secondary key
|
707
|
-
self.class.secondary_key_attributes.all? do |attr|
|
708
|
-
oattrs.include?(attr) or matches_attribute_value?(other, attr, send(attr))
|
709
|
-
end
|
710
|
-
end
|
711
|
-
|
712
|
-
# @param [Attribute] attr_md the attribute to set
|
713
|
-
# @param [Resource] ref the inverse value
|
714
|
-
# @param [Symbol] the inverse => self writer method
|
715
|
-
def delegate_to_inverse_setter(attr_md, ref, writer)
|
716
|
-
logger.debug { "Setting #{qp} #{attr_md} by setting the #{ref.qp} inverse attribute #{attr_md.inverse}..." }
|
717
|
-
ref.send(writer, self)
|
718
|
-
end
|
719
|
-
|
720
|
-
# Returns 0 if attribute is a Java primitive number,
|
721
|
-
# +false+ if attribute is a Java primitive boolean,
|
722
|
-
# an empty collectin if the Java property is a collection,
|
723
|
-
# nil otherwise.
|
724
|
-
def empty_value(attribute)
|
725
|
-
type = java_type(attribute) || return
|
726
|
-
if type.primitive? then
|
727
|
-
type.name == 'boolean' ? false : 0
|
728
|
-
else
|
729
|
-
self.class.empty_value(attribute)
|
730
|
-
end
|
731
|
-
end
|
732
|
-
|
733
|
-
# Returns the Java type of the given attribute, or nil if attribute is not a Java property attribute.
|
734
|
-
def java_type(attribute)
|
735
|
-
attr_md = self.class.attribute_metadata(attribute)
|
736
|
-
attr_md.property_descriptor.property_type if JavaAttribute === attr_md
|
737
|
-
end
|
738
|
-
|
739
|
-
# Executes the given block with the database lazy loader disabled, if any.
|
740
|
-
#
|
741
|
-
# @yield the block to execute
|
742
|
-
def do_without_lazy_loader(&block)
|
743
|
-
if database then
|
744
|
-
database.lazy_loader.disable(&block)
|
745
|
-
else
|
746
|
-
yield
|
747
|
-
end
|
748
|
-
end
|
749
|
-
|
750
|
-
# Returns the source => target hash of matches for the given attr_md newval sources and
|
751
|
-
# oldval targets. If the matcher block is given, then that block is called on the sources
|
752
|
-
# and targets. Otherwise, {Resource.match_all} is called.
|
753
|
-
#
|
754
|
-
# @param [Attribute] attr_md the attribute to match
|
755
|
-
# @param newval the source value
|
756
|
-
# @param oldval the target value
|
757
|
-
# @yield [sources, targets] matches sources to targets
|
758
|
-
# @yieldparam [<Resource>] sources an Enumerable on the source value
|
759
|
-
# @yieldparam [<Resource>] targets an Enumerable on the target value
|
760
|
-
# @return [{Resource => Resource}] the source => target matches
|
761
|
-
def match_attribute_value(attr_md, newval, oldval)
|
762
|
-
# make Enumerable targets and sources for matching
|
763
|
-
sources = newval.to_enum
|
764
|
-
targets = oldval.to_enum
|
765
|
-
|
766
|
-
# match sources to targets
|
767
|
-
logger.debug { "Matching source #{newval.qp} to target #{qp} #{attr_md} #{oldval.qp}..." } unless oldval.nil_or_empty?
|
768
|
-
matches = block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
|
769
|
-
logger.debug { "Matched #{qp} #{attr_md}: #{matches.qp}." } unless matches.empty?
|
770
|
-
matches
|
771
|
-
end
|
772
|
-
|
773
|
-
# Returns the object in others which uniquely matches this domain object on the given attributes,
|
774
|
-
# or nil if there is no unique match. This method returns nil if any attributes value is nil.
|
775
|
-
def match_unique_object_with_attributes(others, attributes)
|
776
|
-
vh = value_hash(attributes)
|
777
|
-
return if vh.empty? or vh.size < attributes.size
|
778
|
-
matches = match_attribute_values(others, vh)
|
779
|
-
matches.first if matches.size == 1
|
780
|
-
end
|
781
|
-
|
782
|
-
# Returns the domain objects in others whose class is the same as this object's class
|
783
|
-
# and whose attribute values equal those in the given attr_value_hash.
|
784
|
-
def match_attribute_values(others, attr_value_hash)
|
785
|
-
others.select do |other|
|
786
|
-
self.class === other and attr_value_hash.all? do |attr, value|
|
787
|
-
matches_attribute_value?(other, attr, value)
|
788
|
-
end
|
789
|
-
end
|
790
|
-
end
|
791
|
-
|
792
|
-
# Returns whether this Resource's attribute value matches the fetched other attribute.
|
793
|
-
# A domain attribute match is determined by {#match?}.
|
794
|
-
# A non-domain attribute match is determined by an equality comparison.
|
795
|
-
def matches_attribute_value?(other, attribute, value)
|
796
|
-
other_val = other.send(attribute)
|
797
|
-
if Resource === value then
|
798
|
-
value.match?(other_val)
|
799
|
-
else
|
800
|
-
value == other_val
|
801
|
-
end
|
802
|
-
end
|
803
|
-
|
804
|
-
# Returns the attribute => value hash to use for matching this domain object as follows:
|
805
|
-
# * If this domain object has a database identifier, then the identifier is the sole match criterion attribute.
|
806
|
-
# * Otherwise, if a secondary key is defined for the object's class, then those attributes are used.
|
807
|
-
# * Otherwise, all attributes are used.
|
808
|
-
#
|
809
|
-
# If any secondary key value is nil, then this method returns an empty hash, since the search is ambiguous.
|
810
|
-
def search_attribute_values
|
811
|
-
# if this object has a database identifier, then the identifier is the search criterion
|
812
|
-
identifier.nil? ? non_id_search_attribute_values : { :identifier => identifier }
|
813
|
-
end
|
814
|
-
|
815
|
-
# Returns the attribute => value hash to use for matching this domain object.
|
816
|
-
# @see #search_attribute_values the method specification
|
817
|
-
def non_id_search_attribute_values
|
818
|
-
# if there is a secondary key, then search on those attributes.
|
819
|
-
# otherwise, search on all attributes.
|
820
|
-
key_attrs = self.class.secondary_key_attributes
|
821
|
-
attrs = key_attrs.empty? ? self.class.nondomain_java_attributes : key_attrs
|
822
|
-
# associate the values
|
823
|
-
attr_values = attrs.to_compact_hash { |attr| send(attr) }
|
824
|
-
# if there is no secondary key, then cull empty values
|
825
|
-
key_attrs.empty? ? attr_values.delete_if { |attr, value| value.nil? } : attr_values
|
826
|
-
end
|
21
|
+
include CaRuby::Migratable, CaRuby::Persistable, Jinx::JSON::Serializer, Jinx::Resource
|
827
22
|
end
|
828
|
-
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|