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.
Files changed (149) hide show
  1. data/.gitignore +14 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +27 -0
  6. data/History.md +6 -0
  7. data/LEGAL +5 -0
  8. data/LICENSE +22 -0
  9. data/README.md +44 -0
  10. data/Rakefile +41 -0
  11. data/examples/family/README.md +10 -0
  12. data/examples/family/ext/build.xml +35 -0
  13. data/examples/family/ext/src/family/Address.java +68 -0
  14. data/examples/family/ext/src/family/Child.java +24 -0
  15. data/examples/family/ext/src/family/DomainObject.java +26 -0
  16. data/examples/family/ext/src/family/Household.java +36 -0
  17. data/examples/family/ext/src/family/Parent.java +48 -0
  18. data/examples/family/ext/src/family/Person.java +42 -0
  19. data/examples/family/lib/family.rb +15 -0
  20. data/examples/family/lib/family/address.rb +6 -0
  21. data/examples/family/lib/family/domain_object.rb +6 -0
  22. data/examples/family/lib/family/household.rb +6 -0
  23. data/examples/family/lib/family/parent.rb +16 -0
  24. data/examples/family/lib/family/person.rb +6 -0
  25. data/examples/model/README.md +25 -0
  26. data/examples/model/ext/build.xml +35 -0
  27. data/examples/model/ext/src/domain/Child.java +192 -0
  28. data/examples/model/ext/src/domain/Dependent.java +29 -0
  29. data/examples/model/ext/src/domain/DomainObject.java +26 -0
  30. data/examples/model/ext/src/domain/Independent.java +83 -0
  31. data/examples/model/ext/src/domain/Parent.java +129 -0
  32. data/examples/model/ext/src/domain/Person.java +14 -0
  33. data/examples/model/lib/model.rb +13 -0
  34. data/examples/model/lib/model/child.rb +13 -0
  35. data/examples/model/lib/model/domain_object.rb +6 -0
  36. data/examples/model/lib/model/independent.rb +11 -0
  37. data/examples/model/lib/model/parent.rb +17 -0
  38. data/jinx.gemspec +22 -0
  39. data/lib/jinx.rb +3 -0
  40. data/lib/jinx/active_support/README.txt +2 -0
  41. data/lib/jinx/active_support/core_ext/string.rb +7 -0
  42. data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
  43. data/lib/jinx/active_support/inflections.rb +55 -0
  44. data/lib/jinx/active_support/inflector.rb +398 -0
  45. data/lib/jinx/cli/application.rb +36 -0
  46. data/lib/jinx/cli/command.rb +214 -0
  47. data/lib/jinx/helpers/array.rb +108 -0
  48. data/lib/jinx/helpers/boolean.rb +42 -0
  49. data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
  50. data/lib/jinx/helpers/class.rb +149 -0
  51. data/lib/jinx/helpers/collection.rb +33 -0
  52. data/lib/jinx/helpers/collections.rb +11 -0
  53. data/lib/jinx/helpers/collector.rb +20 -0
  54. data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
  55. data/lib/jinx/helpers/enumerable.rb +242 -0
  56. data/lib/jinx/helpers/enumerate.rb +35 -0
  57. data/lib/jinx/helpers/error.rb +15 -0
  58. data/lib/jinx/helpers/file_separator.rb +65 -0
  59. data/lib/jinx/helpers/filter.rb +52 -0
  60. data/lib/jinx/helpers/flattener.rb +38 -0
  61. data/lib/jinx/helpers/hash.rb +12 -0
  62. data/lib/jinx/helpers/hashable.rb +502 -0
  63. data/lib/jinx/helpers/inflector.rb +36 -0
  64. data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
  65. data/lib/jinx/helpers/lazy_hash.rb +44 -0
  66. data/lib/jinx/helpers/log.rb +106 -0
  67. data/lib/jinx/helpers/math.rb +12 -0
  68. data/lib/jinx/helpers/merge.rb +60 -0
  69. data/lib/jinx/helpers/module.rb +18 -0
  70. data/lib/jinx/helpers/multi_enumerator.rb +31 -0
  71. data/lib/jinx/helpers/options.rb +92 -0
  72. data/lib/jinx/helpers/os.rb +19 -0
  73. data/lib/jinx/helpers/partial_order.rb +37 -0
  74. data/lib/jinx/helpers/pretty_print.rb +207 -0
  75. data/lib/jinx/helpers/set.rb +8 -0
  76. data/lib/jinx/helpers/stopwatch.rb +76 -0
  77. data/lib/jinx/helpers/transformer.rb +24 -0
  78. data/lib/jinx/helpers/transitive_closure.rb +55 -0
  79. data/lib/jinx/helpers/uniquifier.rb +50 -0
  80. data/lib/jinx/helpers/validation.rb +33 -0
  81. data/lib/jinx/helpers/visitor.rb +370 -0
  82. data/lib/jinx/import/class_path_modifier.rb +77 -0
  83. data/lib/jinx/import/java.rb +337 -0
  84. data/lib/jinx/importer.rb +240 -0
  85. data/lib/jinx/metadata.rb +155 -0
  86. data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
  87. data/lib/jinx/metadata/dependency.rb +244 -0
  88. data/lib/jinx/metadata/id_alias.rb +23 -0
  89. data/lib/jinx/metadata/introspector.rb +179 -0
  90. data/lib/jinx/metadata/inverse.rb +170 -0
  91. data/lib/jinx/metadata/java_property.rb +169 -0
  92. data/lib/jinx/metadata/propertied.rb +500 -0
  93. data/lib/jinx/metadata/property.rb +401 -0
  94. data/lib/jinx/metadata/property_characteristics.rb +114 -0
  95. data/lib/jinx/resource.rb +862 -0
  96. data/lib/jinx/resource/copy_visitor.rb +36 -0
  97. data/lib/jinx/resource/inversible.rb +90 -0
  98. data/lib/jinx/resource/match_visitor.rb +180 -0
  99. data/lib/jinx/resource/matcher.rb +20 -0
  100. data/lib/jinx/resource/merge_visitor.rb +73 -0
  101. data/lib/jinx/resource/mergeable.rb +185 -0
  102. data/lib/jinx/resource/reference_enumerator.rb +49 -0
  103. data/lib/jinx/resource/reference_path_visitor.rb +38 -0
  104. data/lib/jinx/resource/reference_visitor.rb +55 -0
  105. data/lib/jinx/resource/unique.rb +35 -0
  106. data/lib/jinx/version.rb +3 -0
  107. data/spec/defaults_spec.rb +30 -0
  108. data/spec/definitions/model/alias/child.rb +5 -0
  109. data/spec/definitions/model/base/child.rb +5 -0
  110. data/spec/definitions/model/base/domain_object.rb +5 -0
  111. data/spec/definitions/model/base/independent.rb +5 -0
  112. data/spec/definitions/model/defaults/child.rb +5 -0
  113. data/spec/definitions/model/dependency/child.rb +5 -0
  114. data/spec/definitions/model/dependency/parent.rb +6 -0
  115. data/spec/definitions/model/inverse/child.rb +5 -0
  116. data/spec/definitions/model/inverse/independent.rb +5 -0
  117. data/spec/definitions/model/inverse/parent.rb +5 -0
  118. data/spec/definitions/model/mandatory/child.rb +6 -0
  119. data/spec/dependency_spec.rb +47 -0
  120. data/spec/family_spec.rb +64 -0
  121. data/spec/inverse_spec.rb +53 -0
  122. data/spec/mandatory_spec.rb +43 -0
  123. data/spec/metadata_spec.rb +68 -0
  124. data/spec/resource_spec.rb +30 -0
  125. data/spec/spec_helper.rb +3 -0
  126. data/spec/support/model.rb +19 -0
  127. data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
  128. data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
  129. data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
  130. data/test/fixtures/mixed/ext/build.xml +35 -0
  131. data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
  132. data/test/helper.rb +7 -0
  133. data/test/lib/jinx/command_test.rb +41 -0
  134. data/test/lib/jinx/helpers/boolean_test.rb +27 -0
  135. data/test/lib/jinx/helpers/class_test.rb +60 -0
  136. data/test/lib/jinx/helpers/collections_test.rb +402 -0
  137. data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
  138. data/test/lib/jinx/helpers/inflector_test.rb +11 -0
  139. data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
  140. data/test/lib/jinx/helpers/module_test.rb +24 -0
  141. data/test/lib/jinx/helpers/options_test.rb +66 -0
  142. data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
  143. data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
  144. data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
  145. data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
  146. data/test/lib/jinx/helpers/visitor_test.rb +288 -0
  147. data/test/lib/jinx/import/java_test.rb +78 -0
  148. data/test/lib/jinx/import/mixed_case_test.rb +16 -0
  149. 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