jinx 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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