jinx 2.1.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/.gitignore +14 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +27 -0
- data/History.md +6 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/Rakefile +41 -0
- data/examples/family/README.md +10 -0
- data/examples/family/ext/build.xml +35 -0
- data/examples/family/ext/src/family/Address.java +68 -0
- data/examples/family/ext/src/family/Child.java +24 -0
- data/examples/family/ext/src/family/DomainObject.java +26 -0
- data/examples/family/ext/src/family/Household.java +36 -0
- data/examples/family/ext/src/family/Parent.java +48 -0
- data/examples/family/ext/src/family/Person.java +42 -0
- data/examples/family/lib/family.rb +15 -0
- data/examples/family/lib/family/address.rb +6 -0
- data/examples/family/lib/family/domain_object.rb +6 -0
- data/examples/family/lib/family/household.rb +6 -0
- data/examples/family/lib/family/parent.rb +16 -0
- data/examples/family/lib/family/person.rb +6 -0
- data/examples/model/README.md +25 -0
- data/examples/model/ext/build.xml +35 -0
- data/examples/model/ext/src/domain/Child.java +192 -0
- data/examples/model/ext/src/domain/Dependent.java +29 -0
- data/examples/model/ext/src/domain/DomainObject.java +26 -0
- data/examples/model/ext/src/domain/Independent.java +83 -0
- data/examples/model/ext/src/domain/Parent.java +129 -0
- data/examples/model/ext/src/domain/Person.java +14 -0
- data/examples/model/lib/model.rb +13 -0
- data/examples/model/lib/model/child.rb +13 -0
- data/examples/model/lib/model/domain_object.rb +6 -0
- data/examples/model/lib/model/independent.rb +11 -0
- data/examples/model/lib/model/parent.rb +17 -0
- data/jinx.gemspec +22 -0
- data/lib/jinx.rb +3 -0
- data/lib/jinx/active_support/README.txt +2 -0
- data/lib/jinx/active_support/core_ext/string.rb +7 -0
- data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/jinx/active_support/inflections.rb +55 -0
- data/lib/jinx/active_support/inflector.rb +398 -0
- data/lib/jinx/cli/application.rb +36 -0
- data/lib/jinx/cli/command.rb +214 -0
- data/lib/jinx/helpers/array.rb +108 -0
- data/lib/jinx/helpers/boolean.rb +42 -0
- data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
- data/lib/jinx/helpers/class.rb +149 -0
- data/lib/jinx/helpers/collection.rb +33 -0
- data/lib/jinx/helpers/collections.rb +11 -0
- data/lib/jinx/helpers/collector.rb +20 -0
- data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
- data/lib/jinx/helpers/enumerable.rb +242 -0
- data/lib/jinx/helpers/enumerate.rb +35 -0
- data/lib/jinx/helpers/error.rb +15 -0
- data/lib/jinx/helpers/file_separator.rb +65 -0
- data/lib/jinx/helpers/filter.rb +52 -0
- data/lib/jinx/helpers/flattener.rb +38 -0
- data/lib/jinx/helpers/hash.rb +12 -0
- data/lib/jinx/helpers/hashable.rb +502 -0
- data/lib/jinx/helpers/inflector.rb +36 -0
- data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
- data/lib/jinx/helpers/lazy_hash.rb +44 -0
- data/lib/jinx/helpers/log.rb +106 -0
- data/lib/jinx/helpers/math.rb +12 -0
- data/lib/jinx/helpers/merge.rb +60 -0
- data/lib/jinx/helpers/module.rb +18 -0
- data/lib/jinx/helpers/multi_enumerator.rb +31 -0
- data/lib/jinx/helpers/options.rb +92 -0
- data/lib/jinx/helpers/os.rb +19 -0
- data/lib/jinx/helpers/partial_order.rb +37 -0
- data/lib/jinx/helpers/pretty_print.rb +207 -0
- data/lib/jinx/helpers/set.rb +8 -0
- data/lib/jinx/helpers/stopwatch.rb +76 -0
- data/lib/jinx/helpers/transformer.rb +24 -0
- data/lib/jinx/helpers/transitive_closure.rb +55 -0
- data/lib/jinx/helpers/uniquifier.rb +50 -0
- data/lib/jinx/helpers/validation.rb +33 -0
- data/lib/jinx/helpers/visitor.rb +370 -0
- data/lib/jinx/import/class_path_modifier.rb +77 -0
- data/lib/jinx/import/java.rb +337 -0
- data/lib/jinx/importer.rb +240 -0
- data/lib/jinx/metadata.rb +155 -0
- data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
- data/lib/jinx/metadata/dependency.rb +244 -0
- data/lib/jinx/metadata/id_alias.rb +23 -0
- data/lib/jinx/metadata/introspector.rb +179 -0
- data/lib/jinx/metadata/inverse.rb +170 -0
- data/lib/jinx/metadata/java_property.rb +169 -0
- data/lib/jinx/metadata/propertied.rb +500 -0
- data/lib/jinx/metadata/property.rb +401 -0
- data/lib/jinx/metadata/property_characteristics.rb +114 -0
- data/lib/jinx/resource.rb +862 -0
- data/lib/jinx/resource/copy_visitor.rb +36 -0
- data/lib/jinx/resource/inversible.rb +90 -0
- data/lib/jinx/resource/match_visitor.rb +180 -0
- data/lib/jinx/resource/matcher.rb +20 -0
- data/lib/jinx/resource/merge_visitor.rb +73 -0
- data/lib/jinx/resource/mergeable.rb +185 -0
- data/lib/jinx/resource/reference_enumerator.rb +49 -0
- data/lib/jinx/resource/reference_path_visitor.rb +38 -0
- data/lib/jinx/resource/reference_visitor.rb +55 -0
- data/lib/jinx/resource/unique.rb +35 -0
- data/lib/jinx/version.rb +3 -0
- data/spec/defaults_spec.rb +30 -0
- data/spec/definitions/model/alias/child.rb +5 -0
- data/spec/definitions/model/base/child.rb +5 -0
- data/spec/definitions/model/base/domain_object.rb +5 -0
- data/spec/definitions/model/base/independent.rb +5 -0
- data/spec/definitions/model/defaults/child.rb +5 -0
- data/spec/definitions/model/dependency/child.rb +5 -0
- data/spec/definitions/model/dependency/parent.rb +6 -0
- data/spec/definitions/model/inverse/child.rb +5 -0
- data/spec/definitions/model/inverse/independent.rb +5 -0
- data/spec/definitions/model/inverse/parent.rb +5 -0
- data/spec/definitions/model/mandatory/child.rb +6 -0
- data/spec/dependency_spec.rb +47 -0
- data/spec/family_spec.rb +64 -0
- data/spec/inverse_spec.rb +53 -0
- data/spec/mandatory_spec.rb +43 -0
- data/spec/metadata_spec.rb +68 -0
- data/spec/resource_spec.rb +30 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/model.rb +19 -0
- data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
- data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
- data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
- data/test/fixtures/mixed/ext/build.xml +35 -0
- data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
- data/test/helper.rb +7 -0
- data/test/lib/jinx/command_test.rb +41 -0
- data/test/lib/jinx/helpers/boolean_test.rb +27 -0
- data/test/lib/jinx/helpers/class_test.rb +60 -0
- data/test/lib/jinx/helpers/collections_test.rb +402 -0
- data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
- data/test/lib/jinx/helpers/inflector_test.rb +11 -0
- data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
- data/test/lib/jinx/helpers/module_test.rb +24 -0
- data/test/lib/jinx/helpers/options_test.rb +66 -0
- data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
- data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
- data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
- data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
- data/test/lib/jinx/helpers/visitor_test.rb +288 -0
- data/test/lib/jinx/import/java_test.rb +78 -0
- data/test/lib/jinx/import/mixed_case_test.rb +16 -0
- metadata +272 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
require 'jinx/helpers/inflector'
|
|
3
|
+
require 'jinx/helpers/pretty_print'
|
|
4
|
+
require 'jinx/helpers/validation'
|
|
5
|
+
require 'jinx/helpers/collections'
|
|
6
|
+
require 'jinx/helpers/collector'
|
|
7
|
+
require 'jinx/importer'
|
|
8
|
+
require 'jinx/resource/matcher'
|
|
9
|
+
require 'jinx/resource/mergeable'
|
|
10
|
+
require 'jinx/resource/reference_enumerator'
|
|
11
|
+
require 'jinx/resource/reference_visitor'
|
|
12
|
+
require 'jinx/resource/reference_path_visitor'
|
|
13
|
+
require 'jinx/resource/inversible'
|
|
14
|
+
|
|
15
|
+
module Jinx
|
|
16
|
+
# This Resource module enhances application domain classes with the following features:
|
|
17
|
+
# * meta-data introspection
|
|
18
|
+
# * dependency
|
|
19
|
+
# * inverse integrity
|
|
20
|
+
# * defaults
|
|
21
|
+
# * validation
|
|
22
|
+
# * copy/merge
|
|
23
|
+
#
|
|
24
|
+
# A application domain module becomes jinxed by including {Resource} and specifying
|
|
25
|
+
# the Java package and optional JRuby class mix-in definitions.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# # The application domain module
|
|
29
|
+
# module Domain
|
|
30
|
+
# include Jinx::Resource
|
|
31
|
+
# # The caTissue Java package name.
|
|
32
|
+
# packages 'app.domain'
|
|
33
|
+
# # The JRuby mix-ins directory.
|
|
34
|
+
# definitions File.expand_path('domain', dirname(__FILE__))
|
|
35
|
+
# end
|
|
36
|
+
module Resource
|
|
37
|
+
include Mergeable, Inversible
|
|
38
|
+
|
|
39
|
+
# @quirk JRuby Bug #5090 - JRuby 1.5 object_id is no longer a reserved method, and results
|
|
40
|
+
# in a String value rather than an Integer (cf. http://jira.codehaus.org/browse/JRUBY-5090).
|
|
41
|
+
# Work-around is to make a proxy object id.
|
|
42
|
+
#
|
|
43
|
+
# @return [Integer] the object id
|
|
44
|
+
def proxy_object_id
|
|
45
|
+
# make a hash code on demand
|
|
46
|
+
@_hc ||= (Object.new.object_id * 31) + 17
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Prints this object's class demodulized name and object id.
|
|
50
|
+
def print_class_and_id
|
|
51
|
+
"#{self.class.qp}@#{proxy_object_id}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
alias :qp :print_class_and_id
|
|
55
|
+
|
|
56
|
+
# Sets the default attribute values for this domain object and its dependents. If this Resource
|
|
57
|
+
# does not have an identifier, then missing attributes are set to the values defined by
|
|
58
|
+
# {Propertied#add_attribute_defaults}.
|
|
59
|
+
#
|
|
60
|
+
# Subclasses should override the private {#add_defaults_local} method rather than this method.
|
|
61
|
+
#
|
|
62
|
+
# @return [Resource] self
|
|
63
|
+
def add_defaults
|
|
64
|
+
# If there is an owner, then delegate to the owner.
|
|
65
|
+
# Otherwise, add defaults to this object.
|
|
66
|
+
par = owner
|
|
67
|
+
if par and par.identifier.nil? then
|
|
68
|
+
logger.debug { "Adding defaults to #{qp} owner #{par.qp}..." }
|
|
69
|
+
par.add_defaults
|
|
70
|
+
else
|
|
71
|
+
logger.debug { "Adding defaults to #{qp} and its dependents..." }
|
|
72
|
+
# apply the local and dependent defaults
|
|
73
|
+
add_defaults_recursive
|
|
74
|
+
end
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Validates this domain object and its #{#dependents} for consistency and completeness.
|
|
79
|
+
# An object is valid if it contains a non-nil value for each mandatory attribute.
|
|
80
|
+
# Objects which have already been validated are skipped.
|
|
81
|
+
#
|
|
82
|
+
# A Resource class should not override this method, but override the private {#validate_local}
|
|
83
|
+
# method instead.
|
|
84
|
+
#
|
|
85
|
+
# @return [Resource] this domain object
|
|
86
|
+
# @raise (see #validate_local)
|
|
87
|
+
def validate
|
|
88
|
+
if not @validated then
|
|
89
|
+
validate_local
|
|
90
|
+
@validated = true
|
|
91
|
+
end
|
|
92
|
+
dependents.each { |dep| dep.validate }
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns a new domain object with the given attributes copied from this domain object.
|
|
97
|
+
# The attributes argument consists of either attribute Symbols or a single Enumerable
|
|
98
|
+
# consisting of Symbols.
|
|
99
|
+
# The default attributes are the {Propertied#nondomain_attributes}.
|
|
100
|
+
#
|
|
101
|
+
# @param [<Symbol>, (<Symbol>)] attributes the attributes to copy
|
|
102
|
+
# @return [Resource] a copy of this domain object
|
|
103
|
+
def copy(*attributes)
|
|
104
|
+
if attributes.empty? then
|
|
105
|
+
attributes = self.class.nondomain_attributes
|
|
106
|
+
elsif Enumerable === attributes.first then
|
|
107
|
+
Jinx.fail(ArgumentError, "#{qp} copy attributes argument is not a Symbol: #{attributes.first}") unless attributes.size == 1
|
|
108
|
+
attributes = attributes.first
|
|
109
|
+
end
|
|
110
|
+
self.class.new.merge_attributes(self, attributes)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Clears the given attribute value. If the current value responds to the +clear+ method,
|
|
114
|
+
# then the current value is cleared. Otherwise, the value is set to {Metadata#empty_value}.
|
|
115
|
+
#
|
|
116
|
+
# @param [Symbol] attribute the attribute to clear
|
|
117
|
+
def clear_attribute(attribute)
|
|
118
|
+
# the current value to clear
|
|
119
|
+
current = send(attribute)
|
|
120
|
+
return if current.nil?
|
|
121
|
+
# call the current value clear if possible.
|
|
122
|
+
# otherwise, set the attribute to the empty value.
|
|
123
|
+
if current.respond_to?(:clear) then
|
|
124
|
+
current.clear
|
|
125
|
+
else
|
|
126
|
+
writer = self.class.property(attribute).writer
|
|
127
|
+
value = self.class.empty_value(attribute)
|
|
128
|
+
send(writer, value)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Sets this domain object's attribute to the value. This method clears the current attribute value,
|
|
133
|
+
# if any, and merges the new value. Merge rather than assignment ensures that a collection type
|
|
134
|
+
# is preserved, e.g. an Array value is assigned to a set domain type by first clearing the set
|
|
135
|
+
# and then merging the array content into the set.
|
|
136
|
+
#
|
|
137
|
+
# @see Mergeable#merge_attribute
|
|
138
|
+
def set_property_value(attribute, value)
|
|
139
|
+
# bail out if the value argument is the current value
|
|
140
|
+
return value if value.equal?(send(attribute))
|
|
141
|
+
clear_attribute(attribute)
|
|
142
|
+
merge_attribute(attribute, value)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Returns the first non-nil {#key_value} for the primary, secondary
|
|
146
|
+
# and alternate key attributes.
|
|
147
|
+
#
|
|
148
|
+
# @return (see #key_value)
|
|
149
|
+
def key(attributes=nil)
|
|
150
|
+
primary_key or secondary_key or alternate_key
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Returns the key for the given key attributes as follows:
|
|
154
|
+
# * If there are no key attributes, then nil.
|
|
155
|
+
# * Otherwise, if any key attribute value is missing, then nil.
|
|
156
|
+
# * Otherwise, if the key attributes is a singleton Array, then the key is the
|
|
157
|
+
# value of the sole key attribute.
|
|
158
|
+
# * Otherwise, the key is an Array of the key attribute values.
|
|
159
|
+
#
|
|
160
|
+
# @param [<Symbol>] attributes the key attributes, or nil for the primary key
|
|
161
|
+
# @return [Array, Object, nil] the key value or values
|
|
162
|
+
def key_value(attributes)
|
|
163
|
+
attributes ||= self.class.primary_key_attributes
|
|
164
|
+
case attributes.size
|
|
165
|
+
when 0 then nil
|
|
166
|
+
when 1 then send(attributes.first)
|
|
167
|
+
else
|
|
168
|
+
key = attributes.map { |pa| send(pa) || return }
|
|
169
|
+
key unless key.empty?
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @return (see #key_value)
|
|
174
|
+
def primary_key
|
|
175
|
+
key_value(self.class.primary_key_attributes)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @return (see #key_value)
|
|
179
|
+
# @see #key
|
|
180
|
+
def secondary_key
|
|
181
|
+
key_value(self.class.secondary_key_attributes)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @return (see #key_value)
|
|
185
|
+
# @see #key
|
|
186
|
+
def alternate_key
|
|
187
|
+
key_value(self.class.alternate_key_attributes)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @return [Resource, nil] the domain object that owns this object, or nil if this object
|
|
191
|
+
# is not dependent on an owner
|
|
192
|
+
def owner
|
|
193
|
+
self.class.owner_attributes.detect_value { |pa| send(pa) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @return [(Property, Resource), nil] the (property, value) pair for which there is an
|
|
197
|
+
# owner reference, or nil if this domain object does not reference an owner
|
|
198
|
+
def effective_owner_property_value
|
|
199
|
+
self.class.owner_properties.detect_value do |op|
|
|
200
|
+
ref = send(op.attribute)
|
|
201
|
+
[op, ref] if ref
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Sets this dependent's owner attribute to the given domain object.
|
|
206
|
+
#
|
|
207
|
+
# @param [Resource] owner the owner domain object
|
|
208
|
+
# @raise [NoMethodError] if this Resource's class does not have exactly one owner attribute
|
|
209
|
+
def owner=(owner)
|
|
210
|
+
pa = self.class.owner_attribute
|
|
211
|
+
if pa.nil? then Jinx.fail(NoMethodError, "#{self.class.qp} does not have a unique owner attribute") end
|
|
212
|
+
set_property_value(pa, owner)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @param [Resource] other the domain object to check
|
|
216
|
+
# @return [Boolean] whether the other domain object is this object's {#owner} or an
|
|
217
|
+
# {#owner_ancestor?} of this object's {#owner}
|
|
218
|
+
def owner_ancestor?(other)
|
|
219
|
+
ownr = self.owner
|
|
220
|
+
ownr and (ownr == other or ownr.owner_ancestor?(other))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @param [Resource] other the domain object to check
|
|
224
|
+
# @return [Boolean] whether the other domain object is a dependent of this object
|
|
225
|
+
# and has an update-only non-domain attribute.
|
|
226
|
+
def dependent_update_only?(other)
|
|
227
|
+
other.owner == self and
|
|
228
|
+
other.class.nondomain_attributes.detect_with_property { |prop| prop.updatable? and not prop.creatable? }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Returns an attribute => value hash for the specified attributes with a non-nil, non-empty value.
|
|
232
|
+
# The default attributes are this domain object's class {Propertied#attributes}.
|
|
233
|
+
# Only non-nil attributes defined by this Resource are included in the result hash.
|
|
234
|
+
#
|
|
235
|
+
# @param [<Symbol>, nil] attributes the attributes to merge
|
|
236
|
+
# @return [{Symbol => Object}] the attribute => value hash
|
|
237
|
+
def value_hash(attributes=nil)
|
|
238
|
+
attributes ||= self.class.attributes
|
|
239
|
+
attributes.to_compact_hash { |pa| send(pa) if self.class.method_defined?(pa) }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Returns the domain object references for the given attributes.
|
|
243
|
+
#
|
|
244
|
+
# @param [<Symbol>, nil] the domain attributes to include, or nil to include all domain attributes
|
|
245
|
+
# @return [<Resource>] the referenced attribute domain object values
|
|
246
|
+
def references(attributes=nil)
|
|
247
|
+
attributes ||= self.class.domain_attributes
|
|
248
|
+
attributes.map { |pa| send(pa) }.flatten.compact
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# @return [Boolean] whether this domain object is dependent on another entity
|
|
252
|
+
def dependent?
|
|
253
|
+
self.class.dependent?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# @return [Boolean] whether this domain object is not dependent on another entity
|
|
257
|
+
def independent?
|
|
258
|
+
not dependent?
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Returns this domain object's dependents. Dependents which have an alternate preferred
|
|
262
|
+
# owner, as described in {#effective_owner_property_value}, are not included in the
|
|
263
|
+
# result.
|
|
264
|
+
#
|
|
265
|
+
# @param [<Property>, Property, nil] property the dependent property or properties
|
|
266
|
+
# (default is all dependent properties)
|
|
267
|
+
# @return [Enumerable] this domain object's direct dependents
|
|
268
|
+
def dependents(properties=nil)
|
|
269
|
+
properties ||= self.class.dependent_attributes.properties
|
|
270
|
+
# Make a reference enumerator that selects only those dependents which do not have
|
|
271
|
+
# an alternate preferred owner.
|
|
272
|
+
ReferenceEnumerator.new(self, properties).filter do |dep|
|
|
273
|
+
# dep is a candidate dependent. dep could have a preferred owner which differs
|
|
274
|
+
# from self. If there is a different preferred owner, then don't call the
|
|
275
|
+
# iteration block.
|
|
276
|
+
oref = dep.owner
|
|
277
|
+
oref.nil? or oref == self
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Returns the attributes which are required for save. This base implementation returns the
|
|
282
|
+
# class {Propertied#mandatory_attributes}. Subclasses can override this method
|
|
283
|
+
# for domain object state-specific refinements.
|
|
284
|
+
#
|
|
285
|
+
# @return [<Symbol>] the required attributes for a save operation
|
|
286
|
+
def mandatory_attributes
|
|
287
|
+
self.class.mandatory_attributes
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Returns the attribute references which directly depend on this owner.
|
|
291
|
+
# The default is the attribute value.
|
|
292
|
+
#
|
|
293
|
+
# Returns an Enumerable. If the value is not already an Enumerable, then this method
|
|
294
|
+
# returns an empty array if value is nil, or a singelton array with value otherwise.
|
|
295
|
+
#
|
|
296
|
+
# If there is more than one owner of a dependent, then subclasses should override this
|
|
297
|
+
# method to select dependents whose dependency path is shorter than an alternative
|
|
298
|
+
# dependency path, e.g. if a Node is owned by both a Graph and a parent
|
|
299
|
+
# Node. In that case, the Graph direct dependents consist of the top-level nodes
|
|
300
|
+
# owned by the Graph but not referenced by another Node.
|
|
301
|
+
#
|
|
302
|
+
# @param [Symbol] attribute the dependent attribute
|
|
303
|
+
# @return [<Resource>] the attribute value, wrapped in an array if necessary
|
|
304
|
+
def direct_dependents(attribute)
|
|
305
|
+
deps = send(attribute)
|
|
306
|
+
case deps
|
|
307
|
+
when Enumerable then deps
|
|
308
|
+
when nil then Array::EMPTY_ARRAY
|
|
309
|
+
else [deps]
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# @param [Resource] the domain object to match
|
|
314
|
+
# @return [Boolean] whether this object matches the fetched other object on class
|
|
315
|
+
# and a primary, secondary or alternate key
|
|
316
|
+
def matches?(other)
|
|
317
|
+
# trivial case
|
|
318
|
+
return true if equal?(other)
|
|
319
|
+
# check the type
|
|
320
|
+
return false unless self.class == other.class
|
|
321
|
+
# match on primary, secondary or alternate key
|
|
322
|
+
matches_key_attributes?(other, self.class.primary_key_attributes) or
|
|
323
|
+
matches_key_attributes?(other, self.class.secondary_key_attributes) or
|
|
324
|
+
matches_key_attributes?(other, self.class.alternate_key_attributes)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Matches this dependent domain object with the others on type and key attributes
|
|
328
|
+
# in the scope of a parent object.
|
|
329
|
+
# Returns the object in others which matches this domain object, or nil if none.
|
|
330
|
+
#
|
|
331
|
+
# The match attributes are, in order:
|
|
332
|
+
# * the primary key
|
|
333
|
+
# * the secondary key
|
|
334
|
+
# * the alternate key
|
|
335
|
+
#
|
|
336
|
+
# This domain object is matched against the others on the above attributes in succession
|
|
337
|
+
# until a unique match is found. The key attribute matches are strict, i.e. each
|
|
338
|
+
# key attribute value must be non-nil and match the other value.
|
|
339
|
+
#
|
|
340
|
+
# @param [<Resource>] the candidate domain object matches
|
|
341
|
+
# @return [Resource, nil] the matching domain object, or nil if no match
|
|
342
|
+
def match_in(others)
|
|
343
|
+
# trivial case: self is in others
|
|
344
|
+
return self if others.include?(self)
|
|
345
|
+
# filter for the same type
|
|
346
|
+
unless others.all? { |other| self.class === other } then
|
|
347
|
+
others = others.filter { |other| self.class === other }
|
|
348
|
+
end
|
|
349
|
+
# match on primary, secondary or alternate key
|
|
350
|
+
match_unique_object_with_attributes(others, self.class.primary_key_attributes) or
|
|
351
|
+
match_unique_object_with_attributes(others, self.class.secondary_key_attributes) or
|
|
352
|
+
match_unique_object_with_attributes(others, self.class.alternate_key_attributes)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Returns the match of this domain object in the scope of a matching owner as follows:
|
|
356
|
+
# * If {#match_in} returns a match, then that match is the result is used.
|
|
357
|
+
# * Otherwise, if this is a dependent attribute then the match is attempted on a
|
|
358
|
+
# secondary key without owner attributes. Defaults are added to this object in order
|
|
359
|
+
# to pick up potential secondary key values.
|
|
360
|
+
#
|
|
361
|
+
# @param (see #match_in)
|
|
362
|
+
# @return (see #match_in)
|
|
363
|
+
def match_in_owner_scope(others)
|
|
364
|
+
match_in(others) or others.detect { |other| matches_without_owner_attribute?(other) }
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# @return [{Resouce => Resource}] a source => target hash of the given sources which match
|
|
368
|
+
# the targets using the {#match_in} method
|
|
369
|
+
def self.match_all(sources, targets)
|
|
370
|
+
DEF_MATCHER.match(sources, targets)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Returns the difference between this Persistable and the other Persistable for the
|
|
374
|
+
# given attributes. The default attributes are the {Propertied#nondomain_attributes}.
|
|
375
|
+
#
|
|
376
|
+
# @param [Resource] other the domain object to compare
|
|
377
|
+
# @param [<Symbol>, nil] attributes the attributes to compare
|
|
378
|
+
# @return (see Hashable#diff)
|
|
379
|
+
def diff(other, attributes=nil)
|
|
380
|
+
attributes ||= self.class.nondomain_attributes
|
|
381
|
+
vh = value_hash(attributes)
|
|
382
|
+
ovh = other.value_hash(attributes)
|
|
383
|
+
vh.diff(ovh) { |key, v1, v2| Resource.value_equal?(v1, v2) }
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Returns the domain object in others which matches this dependent domain object
|
|
387
|
+
# within the scope of a parent on a minimally acceptable constraint. This method
|
|
388
|
+
# is used when this object might be partially complete--say, lacking a secondary key
|
|
389
|
+
# value--but is expected to match one of the others, e.g. when matching a referenced
|
|
390
|
+
# object to its fetched counterpart.
|
|
391
|
+
#
|
|
392
|
+
# This base implementation returns whether the following conditions hold:
|
|
393
|
+
# 1. other is the same class as this domain object
|
|
394
|
+
# 2. if both identifiers are non-nil, then they are equal
|
|
395
|
+
#
|
|
396
|
+
# Subclasses can override this method to impose additional minimal consistency constraints.
|
|
397
|
+
#
|
|
398
|
+
# @param [Resource] other the domain object to match against
|
|
399
|
+
# @return [Boolean] whether this Resource equals other
|
|
400
|
+
def minimal_match?(other)
|
|
401
|
+
self.class === other and
|
|
402
|
+
(identifier.nil? or other.identifier.nil? or identifier == other.identifier)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Returns an enumerator on the transitive closure of the reference attributes.
|
|
406
|
+
# If a block is given to this method, then the block called on each reference determines
|
|
407
|
+
# which attributes to visit. Otherwise, all saved references are visited.
|
|
408
|
+
#
|
|
409
|
+
# @yield [ref] reference visit attribute selector
|
|
410
|
+
# @yieldparam [Resource] ref the domain object to visit
|
|
411
|
+
# @return [Enumerable] the reference transitive closure
|
|
412
|
+
def reference_hierarchy
|
|
413
|
+
ReferenceVisitor.new { |ref| yield ref }.to_enum(self)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Returns the value for the given attribute path Array or String expression, e.g.:
|
|
417
|
+
# study.path_value("site.address.state")
|
|
418
|
+
# follows the +study+ -> +site+ -> +address+ -> +state+ accessors and returns the +state+
|
|
419
|
+
# value, or nil if any intermediate reference is nil.
|
|
420
|
+
# The array form for the above example is:
|
|
421
|
+
# study.path_value([:site, :address, :state])
|
|
422
|
+
#
|
|
423
|
+
# @param [<Symbol>] path the attributes to navigate
|
|
424
|
+
# @return the attribute navigation result
|
|
425
|
+
def path_value(path)
|
|
426
|
+
path = path.split('.').map { |pa| pa.to_sym } if String === path
|
|
427
|
+
path.inject(self) do |parent, pa|
|
|
428
|
+
value = parent.send(pa)
|
|
429
|
+
return if value.nil?
|
|
430
|
+
value
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Applies the operator block to this object and each domain object in the reference path.
|
|
435
|
+
# This method visits the transitive closure of each recursive path attribute.
|
|
436
|
+
#
|
|
437
|
+
# @param [<Symbol>] path the attributes to visit
|
|
438
|
+
# @yieldparam [Symbol] attribute the attribute to visit
|
|
439
|
+
# @return the visit result
|
|
440
|
+
# @see ReferencePathVisitor
|
|
441
|
+
def visit_path(*path, &operator)
|
|
442
|
+
visitor = ReferencePathVisitor.new(self.class, path)
|
|
443
|
+
visitor.visit(self, &operator)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Applies the operator block to the transitive closure of this domain object's dependency relation.
|
|
447
|
+
# The block argument is a dependent.
|
|
448
|
+
#
|
|
449
|
+
# @yield [dep] operation on the visited domain object
|
|
450
|
+
# @yieldparam [Resource] dep the domain object to visit
|
|
451
|
+
def visit_dependents(&operator) # :yields: dependent
|
|
452
|
+
DEPENDENT_VISITOR.visit(self, &operator)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Applies the operator block to the transitive closure of this domain object's owner relation.
|
|
456
|
+
#
|
|
457
|
+
# @yield [dep] operation on the visited domain object
|
|
458
|
+
# @yieldparam [Resource] dep the domain object to visit
|
|
459
|
+
def visit_owners(&operator) # :yields: owner
|
|
460
|
+
ref = owner
|
|
461
|
+
yield(ref) and ref.visit_owners(&operator) if ref
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# @param q the PrettyPrint queue
|
|
465
|
+
# @return [String] the formatted content of this Resource
|
|
466
|
+
def pretty_print(q)
|
|
467
|
+
q.text(qp)
|
|
468
|
+
content = printable_content
|
|
469
|
+
q.pp_hash(content) unless content.empty?
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Prints this domain object's content and recursively prints the referenced content.
|
|
473
|
+
# The optional selector block determines the attributes to print. The default is the
|
|
474
|
+
# {Propertied#java_attributes}.
|
|
475
|
+
#
|
|
476
|
+
#
|
|
477
|
+
# TODO caRuby override to do_without_lazy_loader
|
|
478
|
+
#
|
|
479
|
+
# @yield [owner] the owner attribute selector
|
|
480
|
+
# @yieldparam [Resource] owner the domain object to print
|
|
481
|
+
# @return [String] the domain object content
|
|
482
|
+
def dump(&selector)
|
|
483
|
+
DetailPrinter.new(self, &selector).pp_s
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Prints this domain object in the format:
|
|
487
|
+
# class_name@object_id{attribute => value ...}
|
|
488
|
+
# The default attributes include identifying attributes.
|
|
489
|
+
#
|
|
490
|
+
# @param [<Symbol>] attributes the attributes to print
|
|
491
|
+
# @return [String] the formatted content
|
|
492
|
+
def to_s(attributes=nil)
|
|
493
|
+
content = printable_content(attributes)
|
|
494
|
+
content_s = content.pp_s(:single_line) unless content.empty?
|
|
495
|
+
"#{print_class_and_id}#{content_s}"
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
alias :inspect :to_s
|
|
499
|
+
|
|
500
|
+
# Returns this domain object's attributes content as an attribute => value hash
|
|
501
|
+
# suitable for printing.
|
|
502
|
+
#
|
|
503
|
+
# The default attributes are this object's saved attributes. The optional
|
|
504
|
+
# reference_printer is used to print a referenced domain object.
|
|
505
|
+
#
|
|
506
|
+
# @param [<Symbol>, nil] attributes the attributes to print
|
|
507
|
+
# @yield [ref] the reference print formatter
|
|
508
|
+
# @yieldparam [Resource] ref the referenced domain object to print
|
|
509
|
+
# @return [{Symbol => String}] the attribute => content hash
|
|
510
|
+
def printable_content(attributes=nil, &reference_printer)
|
|
511
|
+
attributes ||= printworthy_attributes
|
|
512
|
+
vh = value_hash(attributes)
|
|
513
|
+
vh.transform_value { |value| printable_value(value, &reference_printer) }
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Returns whether value equals other modulo the given matches according to the following tests:
|
|
517
|
+
# * _value_ == _other_
|
|
518
|
+
# * _value_ and _other_ are Resource instances and _value_ is a {#match?} with _other_.
|
|
519
|
+
# * _value_ and _other_ are Enumerable with members equal according to the above conditions.
|
|
520
|
+
# * _value_ and _other_ are DateTime instances and are equal to within one second.
|
|
521
|
+
#
|
|
522
|
+
# The DateTime comparison accounts for differences in the Ruby -> Java -> Ruby roundtrip
|
|
523
|
+
# of a date attribute, which loses the seconds fraction.
|
|
524
|
+
#
|
|
525
|
+
# @return [Boolean] whether value and other are equal according to the above tests
|
|
526
|
+
def self.value_equal?(value, other, matches=nil)
|
|
527
|
+
value = value.to_ruby_date if Java::JavaUtil::Date === value
|
|
528
|
+
other = other.to_ruby_date if Java::JavaUtil::Date === other
|
|
529
|
+
if value == other then
|
|
530
|
+
true
|
|
531
|
+
elsif value.collection? and other.collection? then
|
|
532
|
+
collection_value_equal?(value, other, matches)
|
|
533
|
+
elsif Date === value and Date === other then
|
|
534
|
+
(value - other).abs.floor.zero?
|
|
535
|
+
elsif Resource === value and value.class === other then
|
|
536
|
+
value.matches?(other)
|
|
537
|
+
elsif matches then
|
|
538
|
+
matches[value] == other
|
|
539
|
+
else
|
|
540
|
+
false
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
protected
|
|
545
|
+
|
|
546
|
+
# Returns whether this Resource's attribute value matches the given value.
|
|
547
|
+
# A domain attribute match is determined by {#match?}.
|
|
548
|
+
# A non-domain attribute match is determined by an equality comparison.
|
|
549
|
+
#
|
|
550
|
+
# @param [Symbol] attribute the attribute to match
|
|
551
|
+
# @param value the value to compare
|
|
552
|
+
# @return [Boolean] whether the values match
|
|
553
|
+
def matches_attribute_value?(attribute, value)
|
|
554
|
+
v = send(attribute)
|
|
555
|
+
Resource === v ? value.matches?(v) : value == v
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# @return [<Symbol>] the required attributes for this domain object which are nil or empty
|
|
559
|
+
def missing_mandatory_attributes
|
|
560
|
+
mandatory_attributes.select { |pa| send(pa).nil_or_empty? }
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Adds the default values to this object, if necessary, and its dependents.
|
|
564
|
+
#
|
|
565
|
+
# @see #each_defaultable_reference
|
|
566
|
+
def add_defaults_recursive
|
|
567
|
+
# Add the local defaults.
|
|
568
|
+
add_defaults_local
|
|
569
|
+
# Recurse to the dependents.
|
|
570
|
+
each_defaultable_reference { |ref| ref.add_defaults_recursive }
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
private
|
|
574
|
+
|
|
575
|
+
# The copy merge call options.
|
|
576
|
+
# @private
|
|
577
|
+
COPY_MERGE_OPTS = {:inverse => false}
|
|
578
|
+
|
|
579
|
+
# The dependent attribute visitor.
|
|
580
|
+
#
|
|
581
|
+
# @see #visit_dependents
|
|
582
|
+
# @private
|
|
583
|
+
DEPENDENT_VISITOR = Jinx::ReferenceVisitor.new { |obj| obj.class.dependent_attributes }
|
|
584
|
+
|
|
585
|
+
DEF_MATCHER = Matcher.new
|
|
586
|
+
|
|
587
|
+
# Sets the default attribute values for this domain object. Unlike {#add_defaults}, this
|
|
588
|
+
# method does not set defaults for dependents. This method sets the configuration values
|
|
589
|
+
# for this domain object as described in {#add_defaults}, but does not set defaults for
|
|
590
|
+
# dependents.
|
|
591
|
+
#
|
|
592
|
+
# This method is the integration point for subclasses to augment defaults with programmatic
|
|
593
|
+
# logic. If a subclass overrides this method, then it should call super before setting the
|
|
594
|
+
# local default attributes. This ensures that configuration defaults takes precedence.
|
|
595
|
+
def add_defaults_local
|
|
596
|
+
logger.debug { "Adding defaults to #{qp}..." }
|
|
597
|
+
merge_attributes(self.class.defaults)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Validates that this domain object is internally consistent.
|
|
601
|
+
# Subclasses override this method for additional validation, but should call super first.
|
|
602
|
+
#
|
|
603
|
+
# @see #validate_mandatory_attributes
|
|
604
|
+
# @see #validate_owner
|
|
605
|
+
def validate_local
|
|
606
|
+
validate_mandatory_attributes
|
|
607
|
+
validate_owner
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Validates that this domain object contains a non-nil value for each mandatory attribute.
|
|
611
|
+
#
|
|
612
|
+
# @raise [ValidationError] if a mandatory attribute value is missing
|
|
613
|
+
def validate_mandatory_attributes
|
|
614
|
+
invalid = missing_mandatory_attributes
|
|
615
|
+
unless invalid.empty? then
|
|
616
|
+
logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
|
|
617
|
+
Jinx.fail(ValidationError, "Required attribute value missing for #{self}: #{invalid.join(', ')}")
|
|
618
|
+
end
|
|
619
|
+
validate_owner
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Validates that this domain object either doesn't have an owner attribute or has a unique
|
|
623
|
+
# effective owner.
|
|
624
|
+
#
|
|
625
|
+
# @raise [ValidationError] if there is an owner reference attribute that is not set
|
|
626
|
+
# @raise [ValidationError] if there is more than effective owner
|
|
627
|
+
def validate_owner
|
|
628
|
+
# If there is an unambigous owner, then we are done.
|
|
629
|
+
return unless owner.nil?
|
|
630
|
+
# If there is more than one owner attribute, then check that there is at most one
|
|
631
|
+
# unambiguous owner reference. The owner method returns nil if the owner is ambiguous.
|
|
632
|
+
if self.class.owner_attributes.size > 1 then
|
|
633
|
+
vh = value_hash(self.class.owner_attributes)
|
|
634
|
+
if vh.size > 1 then
|
|
635
|
+
Jinx.fail(ValidationError, "Dependent #{self} references multiple owners #{vh.pp_s}:\n#{dump}")
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
# If there is an owner reference attribute, then there must be an owner.
|
|
639
|
+
if self.class.bidirectional_dependent? then
|
|
640
|
+
Jinx.fail(ValidationError, "Dependent #{self} does not reference an owner")
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Enumerates referenced domain objects for setting defaults. This base implementation
|
|
645
|
+
# includes the {#dependents}. Subclasses can override this# method to add references
|
|
646
|
+
# which should be defaulted or to set the order in which defaults are applied.
|
|
647
|
+
#
|
|
648
|
+
# @yield [dep] operate on the dependent
|
|
649
|
+
# @yieldparam [<Resource>] dep the dependent to which the defaults are applied
|
|
650
|
+
def each_defaultable_reference(&block)
|
|
651
|
+
dependents.each(&block)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def self.collection_value_equal?(value, other, matches=nil)
|
|
655
|
+
value.size == other.size and value.all? { |v| other.include?(v) or (matches and other.include?(matches[v])) }
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# A DetailPrinter formats a domain object value for printing using {#to_s} the first time the object
|
|
659
|
+
# is encountered and a ReferencePrinter on the object subsequently.
|
|
660
|
+
# @private
|
|
661
|
+
class DetailPrinter
|
|
662
|
+
alias :to_s :pp_s
|
|
663
|
+
|
|
664
|
+
alias :inspect :to_s
|
|
665
|
+
|
|
666
|
+
# Creates a DetailPrinter on the base object.
|
|
667
|
+
def initialize(base, visited=Set.new, &selector)
|
|
668
|
+
@base = base
|
|
669
|
+
@visited = visited << base
|
|
670
|
+
@selector = selector || Proc.new { |ref| ref.class.printable_attributes }
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def pretty_print(q)
|
|
674
|
+
q.text(@base.qp)
|
|
675
|
+
# pretty-print the standard attribute values
|
|
676
|
+
pas = @selector.call(@base)
|
|
677
|
+
content = @base.printable_content(pas) do |ref|
|
|
678
|
+
if @visited.include?(ref) then
|
|
679
|
+
ReferencePrinter.new(ref)
|
|
680
|
+
else
|
|
681
|
+
DetailPrinter.new(ref, @visited) { |ref| @selector.call(ref) }
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
q.pp_hash(content)
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# A ReferencePrinter formats a reference domain object value for printing with just the class and Ruby object_id.
|
|
689
|
+
# @private
|
|
690
|
+
class ReferencePrinter
|
|
691
|
+
extend Forwardable
|
|
692
|
+
|
|
693
|
+
def_delegator(:@base, :qp, :to_s)
|
|
694
|
+
|
|
695
|
+
alias :inspect :to_s
|
|
696
|
+
|
|
697
|
+
# Creates a ReferencePrinter on the base object.
|
|
698
|
+
def initialize(base)
|
|
699
|
+
@base = base
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Returns a value suitable for printing. If value is a domain object, then the block provided to this method is called.
|
|
704
|
+
# The default block creates a new ReferencePrinter on the value.
|
|
705
|
+
def printable_value(value, &reference_printer)
|
|
706
|
+
Jinx::Collector.on(value) do |item|
|
|
707
|
+
if Resource === item then
|
|
708
|
+
block_given? ? yield(item) : printable_value(item) { |ref| ReferencePrinter.new(ref) }
|
|
709
|
+
else
|
|
710
|
+
item
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Returns an attribute => value hash which identifies the object.
|
|
716
|
+
# If this object has a complete primary key, than the primary key attributes are returned.
|
|
717
|
+
# Otherwise, if there are secondary key attributes, then they are returned.
|
|
718
|
+
# Otherwise, if there are nondomain attributes, then they are returned.
|
|
719
|
+
# Otherwise, if there are fetched attributes, then they are returned.
|
|
720
|
+
#
|
|
721
|
+
# @return [<Symbol] the attributes to print
|
|
722
|
+
def printworthy_attributes
|
|
723
|
+
if self.class.primary_key_attributes.all? { |pa| !!send(pa) } then
|
|
724
|
+
self.class.primary_key_attributes
|
|
725
|
+
elsif not self.class.secondary_key_attributes.empty? then
|
|
726
|
+
self.class.secondary_key_attributes
|
|
727
|
+
elsif not self.class.nondomain_java_attributes.empty? then
|
|
728
|
+
self.class.nondomain_java_attributes
|
|
729
|
+
else
|
|
730
|
+
self.class.fetched_attributes
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Returns whether this domain object matches the other domain object as follows:
|
|
735
|
+
# * The classes are the same.
|
|
736
|
+
# * There are not conflicting primary key values.
|
|
737
|
+
# * Each non-owner secondary key value matches.
|
|
738
|
+
#
|
|
739
|
+
# Note that objects without a secondary key match.
|
|
740
|
+
#
|
|
741
|
+
# @param (see #match_in)
|
|
742
|
+
# @return [Boolean] whether there is a non-owner match
|
|
743
|
+
def matches_without_owner_attribute?(other)
|
|
744
|
+
return false unless other.class == self.class
|
|
745
|
+
# check the primary key
|
|
746
|
+
return false unless self.class.primary_key_attributes.all? do |ka|
|
|
747
|
+
kv = send(ka)
|
|
748
|
+
okv = other.send(ka)
|
|
749
|
+
kv.nil? or okv.nil? or kv == okv
|
|
750
|
+
end
|
|
751
|
+
# match on the non-owner secondary key
|
|
752
|
+
oas = self.class.owner_attributes
|
|
753
|
+
self.class.secondary_key_attributes.all? do |ka|
|
|
754
|
+
oas.include?(ka) or other.matches_attribute_value?(ka, send(ka))
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# @param [Property] prop the attribute to set
|
|
759
|
+
# @param [Resource] ref the inverse value
|
|
760
|
+
# @param [Symbol] the inverse => self writer method
|
|
761
|
+
def delegate_to_inverse_setter(prop, ref, writer)
|
|
762
|
+
logger.debug { "Setting #{qp} #{prop} by setting the #{ref.qp} inverse attribute #{prop.inverse}..." }
|
|
763
|
+
ref.send(writer, self)
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# Returns 0 if attribute is a Java primitive number,
|
|
767
|
+
# +false+ if attribute is a Java primitive boolean,
|
|
768
|
+
# an empty collectin if the Java attribute is a collection,
|
|
769
|
+
# nil otherwise.
|
|
770
|
+
def empty_value(attribute)
|
|
771
|
+
type = java_type(attribute) || return
|
|
772
|
+
if type.primitive? then
|
|
773
|
+
type.name == 'boolean' ? false : 0
|
|
774
|
+
else
|
|
775
|
+
self.class.empty_value(attribute)
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# Returns the Java type of the given attribute, or nil if attribute is not a Java property attribute.
|
|
780
|
+
def java_type(attribute)
|
|
781
|
+
prop = self.class.property(attribute)
|
|
782
|
+
prop.property_descriptor.attribute_type if JavaProperty === prop
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Returns the source => target hash of matches for the given prop newval sources and
|
|
786
|
+
# oldval targets. If the matcher block is given, then that block is called on the sources
|
|
787
|
+
# and targets. Otherwise, {Resource.match_all} is called.
|
|
788
|
+
#
|
|
789
|
+
# @param [Property] prop the attribute to match
|
|
790
|
+
# @param newval the source value
|
|
791
|
+
# @param oldval the target value
|
|
792
|
+
# @yield [sources, targets] matches sources to targets
|
|
793
|
+
# @yieldparam [<Resource>] sources an Enumerable on the source value
|
|
794
|
+
# @yieldparam [<Resource>] targets an Enumerable on the target value
|
|
795
|
+
# @return [{Resource => Resource}] the source => target matches
|
|
796
|
+
def match_attribute_value(prop, newval, oldval)
|
|
797
|
+
# make Enumerable targets and sources for matching
|
|
798
|
+
sources = newval.to_enum
|
|
799
|
+
targets = oldval.to_enum
|
|
800
|
+
|
|
801
|
+
# match sources to targets
|
|
802
|
+
unless oldval.nil_or_empty? then
|
|
803
|
+
logger.debug { "Matching source #{newval.qp} to target #{qp} #{prop} #{oldval.qp}..." }
|
|
804
|
+
end
|
|
805
|
+
matches = block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
|
|
806
|
+
logger.debug { "Matched #{qp} #{prop}: #{matches.qp}." } unless matches.empty?
|
|
807
|
+
matches
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
# @param [<Symbol>] attributes the attributes to match
|
|
811
|
+
# @return [Boolean] whether there is a non-nil value for each attribute and the value matches
|
|
812
|
+
# the other attribute value
|
|
813
|
+
def matches_key_attributes?(other, attributes)
|
|
814
|
+
return false if attributes.empty?
|
|
815
|
+
attributes.all? do |pa|
|
|
816
|
+
v = send(pa)
|
|
817
|
+
if v.nil? then
|
|
818
|
+
false
|
|
819
|
+
else
|
|
820
|
+
ov = other.send(pa)
|
|
821
|
+
Resource === v ? v.matches?(ov) : v == ov
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
# Returns the object in others which uniquely matches this domain object on the given attributes,
|
|
827
|
+
# or nil if there is no unique match. This method returns nil if any attributes value is nil.
|
|
828
|
+
def match_unique_object_with_attributes(others, attributes)
|
|
829
|
+
vh = value_hash(attributes)
|
|
830
|
+
return if vh.empty? or vh.size < attributes.size
|
|
831
|
+
matches = others.select do |other|
|
|
832
|
+
self.class == other.class and
|
|
833
|
+
vh.all? { |pa, v| other.matches_attribute_value?(pa, v) }
|
|
834
|
+
end
|
|
835
|
+
matches.first if matches.size == 1
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# Returns the attribute => value hash to use for matching this domain object as follows:
|
|
839
|
+
# * If this domain object has a database identifier, then the identifier is the sole match criterion attribute.
|
|
840
|
+
# * Otherwise, if a secondary key is defined for the object's class, then those attributes are used.
|
|
841
|
+
# * Otherwise, all attributes are used.
|
|
842
|
+
#
|
|
843
|
+
# If any secondary key value is nil, then this method returns an empty hash, since the search is ambiguous.
|
|
844
|
+
def search_attribute_values
|
|
845
|
+
# if this object has a database identifier, then the identifier is the search criterion
|
|
846
|
+
identifier.nil? ? non_id_search_attribute_values : { :identifier => identifier }
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
# Returns the attribute => value hash to use for matching this domain object.
|
|
850
|
+
# @see #search_attribute_values the method specification
|
|
851
|
+
def non_id_search_attribute_values
|
|
852
|
+
# if there is a secondary key, then search on those attributes.
|
|
853
|
+
# otherwise, search on all attributes.
|
|
854
|
+
key_props = self.class.secondary_key_attributes
|
|
855
|
+
pas = key_props.empty? ? self.class.nondomain_java_attributes : key_props
|
|
856
|
+
# associate the values
|
|
857
|
+
attr_values = pas.to_compact_hash { |pa| send(pa) }
|
|
858
|
+
# if there is no secondary key, then cull empty values
|
|
859
|
+
key_props.empty? ? attr_values.delete_if { |pa, value| value.nil? } : attr_values
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
end
|