caruby-core 1.5.5 → 2.1.1

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