caruby-core 1.4.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 (86) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +51 -0
  5. data/doc/website/css/site.css +1 -5
  6. data/doc/website/images/avatar.png +0 -0
  7. data/doc/website/images/favicon.ico +0 -0
  8. data/doc/website/images/logo.png +0 -0
  9. data/doc/website/index.html +82 -0
  10. data/doc/website/install.html +87 -0
  11. data/doc/website/quick_start.html +87 -0
  12. data/doc/website/tissue.html +85 -0
  13. data/doc/website/uom.html +10 -0
  14. data/lib/caruby.rb +3 -0
  15. data/lib/caruby/active_support/README.txt +2 -0
  16. data/lib/caruby/active_support/core_ext/string.rb +7 -0
  17. data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
  18. data/lib/caruby/active_support/inflections.rb +55 -0
  19. data/lib/caruby/active_support/inflector.rb +398 -0
  20. data/lib/caruby/cli/application.rb +36 -0
  21. data/lib/caruby/cli/command.rb +169 -0
  22. data/lib/caruby/csv/csv_mapper.rb +157 -0
  23. data/lib/caruby/csv/csvio.rb +185 -0
  24. data/lib/caruby/database.rb +252 -0
  25. data/lib/caruby/database/fetched_matcher.rb +66 -0
  26. data/lib/caruby/database/persistable.rb +432 -0
  27. data/lib/caruby/database/persistence_service.rb +162 -0
  28. data/lib/caruby/database/reader.rb +599 -0
  29. data/lib/caruby/database/saved_merger.rb +131 -0
  30. data/lib/caruby/database/search_template_builder.rb +59 -0
  31. data/lib/caruby/database/sql_executor.rb +75 -0
  32. data/lib/caruby/database/store_template_builder.rb +200 -0
  33. data/lib/caruby/database/writer.rb +469 -0
  34. data/lib/caruby/domain/annotatable.rb +25 -0
  35. data/lib/caruby/domain/annotation.rb +23 -0
  36. data/lib/caruby/domain/attribute_metadata.rb +447 -0
  37. data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
  38. data/lib/caruby/domain/merge.rb +91 -0
  39. data/lib/caruby/domain/properties.rb +95 -0
  40. data/lib/caruby/domain/reference_visitor.rb +289 -0
  41. data/lib/caruby/domain/resource_attributes.rb +528 -0
  42. data/lib/caruby/domain/resource_dependency.rb +205 -0
  43. data/lib/caruby/domain/resource_introspection.rb +159 -0
  44. data/lib/caruby/domain/resource_metadata.rb +117 -0
  45. data/lib/caruby/domain/resource_module.rb +285 -0
  46. data/lib/caruby/domain/uniquify.rb +38 -0
  47. data/lib/caruby/import/annotatable_class.rb +28 -0
  48. data/lib/caruby/import/annotation_class.rb +27 -0
  49. data/lib/caruby/import/annotation_module.rb +67 -0
  50. data/lib/caruby/import/java.rb +338 -0
  51. data/lib/caruby/migration/migratable.rb +167 -0
  52. data/lib/caruby/migration/migrator.rb +533 -0
  53. data/lib/caruby/migration/resource.rb +8 -0
  54. data/lib/caruby/migration/resource_module.rb +11 -0
  55. data/lib/caruby/migration/uniquify.rb +20 -0
  56. data/lib/caruby/resource.rb +969 -0
  57. data/lib/caruby/util/attribute_path.rb +46 -0
  58. data/lib/caruby/util/cache.rb +53 -0
  59. data/lib/caruby/util/class.rb +99 -0
  60. data/lib/caruby/util/collection.rb +1053 -0
  61. data/lib/caruby/util/controlled_value.rb +35 -0
  62. data/lib/caruby/util/coordinate.rb +75 -0
  63. data/lib/caruby/util/domain_extent.rb +49 -0
  64. data/lib/caruby/util/file_separator.rb +65 -0
  65. data/lib/caruby/util/inflector.rb +20 -0
  66. data/lib/caruby/util/log.rb +95 -0
  67. data/lib/caruby/util/math.rb +12 -0
  68. data/lib/caruby/util/merge.rb +59 -0
  69. data/lib/caruby/util/module.rb +34 -0
  70. data/lib/caruby/util/options.rb +92 -0
  71. data/lib/caruby/util/partial_order.rb +36 -0
  72. data/lib/caruby/util/person.rb +119 -0
  73. data/lib/caruby/util/pretty_print.rb +184 -0
  74. data/lib/caruby/util/properties.rb +112 -0
  75. data/lib/caruby/util/stopwatch.rb +66 -0
  76. data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
  77. data/lib/caruby/util/transitive_closure.rb +45 -0
  78. data/lib/caruby/util/tree.rb +48 -0
  79. data/lib/caruby/util/trie.rb +37 -0
  80. data/lib/caruby/util/uniquifier.rb +30 -0
  81. data/lib/caruby/util/validation.rb +48 -0
  82. data/lib/caruby/util/version.rb +56 -0
  83. data/lib/caruby/util/visitor.rb +351 -0
  84. data/lib/caruby/util/weak_hash.rb +36 -0
  85. data/lib/caruby/version.rb +3 -0
  86. metadata +186 -0
@@ -0,0 +1,8 @@
1
+ require 'caruby/resource'
2
+ require 'caruby/migration/migratable'
3
+
4
+ module CaRuby
5
+ module Resource
6
+ include Migratable
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ require 'caruby/domain/resource_module'
2
+
3
+ module CaRuby
4
+ module ResourceModule
5
+ # Declares the given classes which will be dynamically modified for migration.
6
+ # The Java caBIG classes are auto-loaded and wrapped as a CaRuby::Resource, if necessary, and enhanced in the migration shim.
7
+ def shims(*classes)
8
+ # nothing to do, since all this method does is ensure that the arguments are auto-loaded when they are referenced
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ require 'caruby/migration/migratable'
2
+ require 'caruby/domain/uniquify'
3
+
4
+ module CaRuby
5
+ module Migratable
6
+ # Unique makes a Migratable Resource domain object unique within the scope its class.
7
+ module Unique
8
+ include CaRuby::Resource::Unique
9
+
10
+ # Augments the migration by making this Resource object unique in the scope of its class.
11
+ #
12
+ # @param (see CaRuby::Migratable#migrate)
13
+ def migrate(row, migrated)
14
+ super
15
+ logger.debug { "Migrator making #{self} unique..." }
16
+ uniquify
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,969 @@
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/merge'
8
+ require 'caruby/domain/reference_visitor'
9
+ require 'caruby/database/persistable'
10
+ require 'caruby/domain/resource_metadata'
11
+ require 'caruby/domain/resource_module'
12
+
13
+ module CaRuby
14
+ # The Domain module is included by Java domain classes.
15
+ # This module defines essential common domain methods that enable the jRuby-Java API bridge.
16
+ # Classes which include Domain must implement the +metadata+ Domain::Metadata accessor method.
17
+ module Resource
18
+ include Comparable, Mergeable, Persistable, Validation
19
+
20
+ # @param [{Symbol => Object}] the optional attribute => value hash
21
+ # @return a new instance of this Resource class initialized from the optional attribute => value hash
22
+ def initialize(hash=nil)
23
+ super()
24
+ if hash then
25
+ unless Hashable === hash then
26
+ raise ArgumentError.new("#{qp} initializer argument type not supported: #{hash.class.qp}")
27
+ end
28
+ merge_attributes(hash)
29
+ end
30
+ end
31
+
32
+ # Returns the database identifier.
33
+ # This method aliases a Java +id+ property accessor, since +id+ is a reserved Ruby attribute.
34
+ def identifier
35
+ getId
36
+ end
37
+
38
+ # Sets the database identifier to the given id value.
39
+ # This method aliases the caTissue Java +id+ property setter,
40
+ # since +id+ is a reserved Ruby attribute.
41
+ def identifier=(id)
42
+ setId(id)
43
+ end
44
+
45
+ # Sets the default attribute values for this domain object and its dependents. If this Resource
46
+ # does not have an identifier, then missing attributes are set to the values defined by
47
+ # {ResourceAttributes#add_attribute_defaults}.
48
+ #
49
+ # _Implementation Note_: subclasses should override the private {#add_defaults_local} method
50
+ # rather than this method.
51
+ #
52
+ # @return [Resource] self
53
+ def add_defaults
54
+ # apply owner defaults
55
+ if owner then
56
+ owner.add_defaults
57
+ else
58
+ logger.debug { "Adding defaults to #{qp} and its dependents..." }
59
+ # apply the local and dependent defaults
60
+ add_defaults_recursive
61
+ end
62
+ self
63
+ end
64
+
65
+ # Validates this domain object and its cascaded dependents for completeness prior to a
66
+ # database create or update operation.
67
+ # The object is valid if it contains a non-nil value for each mandatory property.
68
+ # Objects which have already been validated are skipped.
69
+ # Returns this domain object.
70
+ #
71
+ # @raise [ValidationError] if a mandatory attribute value is missing
72
+ def validate
73
+ unless @validated then
74
+ logger.debug { "Validating #{qp} required attributes #{self.class.mandatory_attributes.to_a.to_series}..." }
75
+ invalid = missing_mandatory_attributes
76
+ unless invalid.empty? then
77
+ logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
78
+ raise ValidationError.new("Required attribute value missing for #{self}: #{invalid.join(', ')}")
79
+ end
80
+ end
81
+ self.class.cascaded_attributes.each do |attr|
82
+ send(attr).enumerate { |dep| dep.validate }
83
+ end
84
+ @validated = true
85
+ self
86
+ end
87
+
88
+ # Returns the attributes to use for a search using this domain object as a template, determined
89
+ # as follows:
90
+ # * If this domain object has a non-nil primary key, then the primary key is the search criterion.
91
+ # * Otherwise, if this domain object has a secondary key and each key attribute value is not nil,
92
+ # then the secondary key is the search criterion.
93
+ # * Otherwise, if this domain object has an alternate key and each key attribute value is not nil,
94
+ # then the aklternate key is the search criterion.
95
+ #
96
+ # @return [<Symbol>] the attributes to use for a search on this domain object
97
+ def searchable_attributes
98
+ key_attrs = self.class.primary_key_attributes
99
+ return key_attrs if key_searchable?(key_attrs)
100
+ key_attrs = self.class.secondary_key_attributes
101
+ return key_attrs if key_searchable?(key_attrs)
102
+ key_attrs = self.class.alternate_key_attributes
103
+ return key_attrs if key_searchable?(key_attrs)
104
+ end
105
+
106
+ # Returns a new domain object with the given attributes copied from this domain object. The attributes
107
+ # argument consists of either attribute Symbols or a single Enumerable consisting of Symbols.
108
+ # The default attributes are the {ResourceAttributes#nondomain_attributes}.
109
+ #
110
+ # @param [<Symbol>] attributes the attributes to copy
111
+ # @return [Resource] a copy of this domain object
112
+ def copy(*attributes)
113
+ if attributes.empty? then
114
+ attributes = self.class.nondomain_attributes
115
+ elsif Enumerable === attributes.first then
116
+ raise ArgumentError.new("#{qp} copy attributes argument is not a Symbol: #{attributes.first}") unless attributes.size == 1
117
+ attributes = attributes.first
118
+ end
119
+ self.class.new.merge_attributes(self, attributes)
120
+ end
121
+
122
+ # Merges the other into this domain object. The non-domain attributes are always merged.
123
+ #
124
+ # @param [Resource] other the merge source domain object
125
+ # @param [<Symbol>] attributes the domain attributes to merge
126
+ # @yield [source, target] mergethe source into the target
127
+ # @yieldparam [Resource] source the reference to merge from
128
+ # @yieldparam [Resource] target the reference the source is merge into
129
+ # @raise [ArgumentError] if other is not an instance of this domain object's class
130
+ # @see #merge_attribute_value
131
+ def merge_match(other, attributes, &matcher)
132
+ raise ArgumentError.new("Incompatible #{qp} merge source: #{other.qp}") unless self.class === other
133
+ logger.debug { format_merge_log_message(other, attributes) }
134
+ # merge the non-domain attributes
135
+ merge_attributes(other)
136
+ # merge the domain attributes
137
+ unless attributes.nil_or_empty? then
138
+ merge_attributes(other, attributes) do |attr, oldval, newval|
139
+ merge_attribute_value(attr, oldval, newval, &matcher)
140
+ end
141
+ end
142
+ end
143
+
144
+ # Merges the attribute newval into oldval as follows:
145
+ # * If attribute is a non-domain attribute and oldval is either nil or the attribute is not saved,
146
+ # then the attribute value is set to newval.
147
+ # * Otherwise, if attribute is a domain non-collection attribute, then newval is recursively
148
+ # merged into oldval.
149
+ # * Otherwise, if attribute is a domain collection attribute, then matching newval members are
150
+ # merged into the corresponding oldval member and non-matching newval members are added to
151
+ # newval.
152
+ # * Otherwise, the attribute value is not changed.
153
+ #
154
+ # The domain value match is performed by the matcher block. The block arguments are newval and
155
+ # oldval and the return value is a source => target hash of matching references. The default
156
+ # matcher is {Resource#match_in}.
157
+ #
158
+ # @param [Symbol] attribute the merge attribute
159
+ # @param oldval the current value
160
+ # @param newval the value to merge
161
+ # @yield [newval, oldval] the value matcher used if attribute is a domain collection
162
+ # @yieldparam newval the merge source value
163
+ # @yieldparam oldval this domain object's current attribute value
164
+ # @return the merged attribute value
165
+ def merge_attribute_value(attribute, oldval, newval, &matcher)
166
+ attr_md = self.class.attribute_metadata(attribute)
167
+ if attr_md.nondomain? then
168
+ if oldval.nil? and not newval.nil? then
169
+ send(attr_md.writer, newval)
170
+ newval
171
+ end
172
+ else
173
+ merge_domain_attribute_value(attr_md, oldval, newval, &matcher)
174
+ end
175
+ end
176
+
177
+ def clear_attribute(attribute)
178
+ # the current value to clear
179
+ current = send(attribute)
180
+ return if current.nil?
181
+ # call the current value clear if possible.
182
+ # otherwise, set the attribute to the empty value.
183
+ if current.respond_to?(:clear) then
184
+ current.clear
185
+ else
186
+ send(self.class.attribute_metadata(attribute).writer, empty_value(attribute))
187
+ end
188
+ end
189
+
190
+ # Sets this domain object's attribute to the value. This method clears the current attribute value,
191
+ # if any, and merges the new value. Merge rather than assignment ensures that a collection type
192
+ # is preserved, e.g. an Array value is assigned to a set domain type by first clearing the set
193
+ # and then merging the array content into the set.
194
+ #
195
+ # @see #merge_attribute
196
+ def set_attribute(attribute, value)
197
+ # bail out if the value argument is the current value
198
+ return value if value.equal?(send(attribute))
199
+ clear_attribute(attribute)
200
+ merge_attribute(attribute, value)
201
+ end
202
+
203
+ # Returns the secondary key attribute values as follows:
204
+ # * If there is no secondary key, then this method returns nil.
205
+ # * Otherwise, if the secondary key attributes is a singleton Array, then the key is the value of the sole key attribute.
206
+ # * Otherwise, the key is an Array of the key attribute values.
207
+ def key
208
+ attrs = self.class.secondary_key_attributes
209
+ case attrs.size
210
+ when 0 then nil
211
+ when 1 then send(attrs.first)
212
+ else attrs.map { |attr| send(attr) }
213
+ end
214
+ end
215
+
216
+ # Returns the domain object that owns this object, or nil if this object is not dependent on an owner.
217
+ def owner
218
+ self.class.owner_attributes.detect_value { |attr| send(attr) }
219
+ end
220
+
221
+ # Returns whether the other domain object is this object's #owner or an #owner_ancestor? of this object's {#owner}.
222
+ def owner_ancestor?(other)
223
+ ref = owner
224
+ ref and (ref == other or ref.owner_ancestor?(other))
225
+ end
226
+
227
+ # Returns an attribute => value hash for the specified attributes with a non-nil, non-empty value.
228
+ # The default attributes are this domain object's class {ResourceAttributes#attributes}.
229
+ # Only non-nil attributes defined by this Resource are included in the result hash.
230
+ #
231
+ # @param [<Symbol>, nil] attributes the attributes to merge
232
+ # @return [{Symbol => Object}] the attribute => value hash
233
+ def value_hash(attributes=nil)
234
+ attributes ||= self.class.attributes
235
+ attributes.to_compact_hash { |attr| send(attr) if self.class.method_defined?(attr) }
236
+ end
237
+
238
+ # Returns the domain object references for the given attributes.
239
+ #
240
+ # @param [<Symbol>, nil] the domain attributes to include, or nil to include all domain attributes
241
+ # @return [<Resource>] the referenced attribute domain object values
242
+ def references(attributes=nil)
243
+ attributes ||= self.class.domain_attributes
244
+ attributes.map { |attr| send(attr) }.flatten.compact
245
+ end
246
+
247
+ # Returns whether this domain object is dependent on another entity.
248
+ def dependent?
249
+ self.class.dependent?
250
+ end
251
+
252
+ # Returns whether this domain object is not dependent on another entity.
253
+ def independent?
254
+ not dependent?
255
+ end
256
+
257
+ # Enumerates over this domain object's dependents.
258
+ #
259
+ # @yield [dep] the block to execute on the dependent
260
+ # @yieldparam [Resource] dep the dependent
261
+ def each_dependent
262
+ self.class.dependent_attributes.each do |attr|
263
+ send(attr).enumerate { |dep| yield dep }
264
+ end
265
+ end
266
+
267
+ # The dependent Enumerable.
268
+ def dependents
269
+ enum_for(:each_dependent)
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
+ def direct_dependents(attribute)
284
+ deps = send(attribute)
285
+ case deps
286
+ when Enumerable then deps
287
+ when nil then Array::EMPTY_ARRAY
288
+ else [deps]
289
+ end
290
+ end
291
+
292
+ # Returns pairs +[+_value1_, _value2_+]+ of correlated non-nil values for every attribute in attributes
293
+ # in this and the other domain object, e.g. given domain objects +site1+ and +site2+ with non-nil
294
+ # +address+ dependents:
295
+ # site1.assoc_attribute_values(site2, [:address]) #=> [site1.address, site2.address]
296
+ def assoc_attribute_values(other, attributes)
297
+ return {} if other.nil? or attributes.empty?
298
+ # associate the attribute => value hashes for this object and the other hash into one attribute => [value1, value2] hash
299
+ value_hash(attributes).assoc_values(other.value_hash(attributes)).values.reject { |values| values.any? { |value| value.nil? } }
300
+ end
301
+
302
+ # Returns whether this object matches the fetched other object on class and key values.
303
+ def match?(other)
304
+ match_in([other])
305
+ end
306
+
307
+ # Matches this dependent domain object with the others on type and key attributes
308
+ # in the scope of a parent object.
309
+ # Returns the object in others which matches this domain object, or nil if none.
310
+ #
311
+ # The match attributes are, in order:
312
+ # * the primary key
313
+ # * the secondary key
314
+ # * the alternate key
315
+ #
316
+ # This domain object is matched against the others on the above attributes in succession
317
+ # until a unique match is found. The key attribute matches are strict, i.e. each
318
+ # key attribute value must be non-nil and match the other value.
319
+ def match_in(others)
320
+ # trivial case: self is in others
321
+ return self if others.include?(self)
322
+ # filter for the same type
323
+ others = others.filter { |other| self.class === other }
324
+ # match on primary, secondary or alternate key
325
+ match_unique_object_with_attributes(others, self.class.primary_key_attributes) or
326
+ match_unique_object_with_attributes(others, self.class.secondary_key_attributes) or
327
+ match_unique_object_with_attributes(others, self.class.alternate_key_attributes)
328
+ end
329
+
330
+ # Returns the match of this domain object in the scope of a matching owner, if any.
331
+ # If this domain object is dependent, then the match is performed in the context of a
332
+ # matching owner object. If {#match_in} returns a match, then that result is used.
333
+ # Otherwise, #match_without_owner_attribute is called.
334
+ #
335
+ # @param [<Resource>] others the candidate domain objects for the match
336
+ # @return [Resource, nil] the matching domain object, if any
337
+ def match_in_owner_scope(others)
338
+ match_in(others) or others.detect { |other| match_without_owner_attribute(other) }
339
+ end
340
+
341
+ # @param [<Resource>] other the domain object to match against
342
+ # @return [Boolean] whether the other domain object matches this domain object on a secondary
343
+ # key without owner attributes
344
+ def match_without_owner_attribute(other)
345
+ oattrs = self.class.owner_attributes
346
+ return if oattrs.empty?
347
+ self.class.secondary_key_attributes.all? do |attr|
348
+ matches_attribute_value?(other, attr, send(attr))
349
+ end
350
+ end
351
+
352
+ # @return [{Resouce => Resource}] a source => target hash of the given sources which match the
353
+ # targets using the {#match_in} method
354
+ def self.match_all(sources, targets)
355
+ DEF_MATCHER.match(sources, targets)
356
+ end
357
+
358
+ # Returns the difference between this Persistable and the other Persistable for the
359
+ # given attributes. The default attributes are the {ResourceAttributes#nondomain_attributes}.
360
+ #
361
+ # @param [Resource] other the domain object to compare
362
+ # @param [<Symbol>, nil] attributes the attributes to compare
363
+ # @return (see Hashable#diff)
364
+ def diff(other, attributes=nil)
365
+ attributes ||= self.class.nondomain_attributes
366
+ vh = value_hash(attributes)
367
+ ovh = other.value_hash(attributes)
368
+ vh.diff(ovh) { |key, v1, v2| Resource.value_equal?(v1, v2) }
369
+ end
370
+
371
+ # Returns the domain object in others which matches this dependent domain object
372
+ # within the scope of a parent on a minimally acceptable constraint. This method
373
+ # is used when this object might be partially complete--say, lacking a secondary key
374
+ # value--but is expected to match one of the others, e.g. when matching a referenced
375
+ # object to its fetched counterpart.
376
+ #
377
+ # This base implementation returns whether the following conditions hold:
378
+ # 1. other is the same class as this domain object
379
+ # 2. if both identifiers are non-nil, then they are equal
380
+ #
381
+ # Subclasses can override this method to impose additional minimal consistency constraints.
382
+ #
383
+ # @param [Resource] other the domain object to match against
384
+ # @return [Boolean] whether this Resource equals other
385
+ def minimal_match?(other)
386
+ self.class === other and
387
+ (identifier.nil? or other.identifier.nil? or identifier == other.identifier)
388
+ end
389
+
390
+ # Returns an enumerator on the transitive closure of the reference attributes.
391
+ # If a block is given to this method, then the block called on each reference determines
392
+ # which attributes to visit. Otherwise, all saved references are visited.
393
+ #
394
+ # @yield [ref] reference visit attribute selector
395
+ # @yieldparam [Resource] ref the domain object to visit
396
+ # @return [Enumerable] the reference transitive closure
397
+ def reference_hierarchy
398
+ ReferenceVisitor.new { |ref| yield ref }.to_enum(self)
399
+ end
400
+
401
+ # Returns the value for the given attribute path Array or String expression, e.g.:
402
+ # study.path_value("site.address.state")
403
+ # follows the +study+ -> +site+ -> +address+ -> +state+ accessors and returns the +state+
404
+ # value, or nil if any intermediate reference is nil.
405
+ # The array form for the above example is:
406
+ # study.path_value([:site, :address, :state])
407
+ #
408
+ # @param [<Symbol>] path the attributes to navigate
409
+ # @return the attribute navigation result
410
+ def path_value(path)
411
+ path = path.split('.').map { |attr| attr.to_sym } if String === path
412
+ path.inject(self) do |parent, attr|
413
+ value = parent.send(attr)
414
+ return if value.nil?
415
+ value
416
+ end
417
+ end
418
+
419
+ # Applies the operator block to this object and each domain object in the reference path.
420
+ # This method visits the transitive closure of each recursive path attribute.
421
+ #
422
+ # For example, given the attributes:
423
+ # treatment: BioMaterial -> Treatment
424
+ # measurement: Treatment -> BioMaterial
425
+ # and +BioMaterial+ instance +biospecimen+, then:
426
+ # biospecimen.visit_path[:treatment, :measurement, :biomaterial]
427
+ # visits +biospecimen+ and all biomaterial, treatments and measurements derived
428
+ # directly or indirectly from +biospecimen+.
429
+ #
430
+ # @param [<Symbol>] path the attributes to visit
431
+ # @yieldparam [Symbol] attribute the attribute to visit
432
+ # @return the visit result
433
+ def visit_path(path, &operator)
434
+ visitor = ReferencePathVisitorFactory.create(self.class, path)
435
+ visitor.visit(self, &operator)
436
+ end
437
+
438
+ # Applies the operator block to the transitive closure of this domain object's dependency relation.
439
+ # The block argument is a dependent.
440
+ #
441
+ # @yield [dep] operation on the visited domain object
442
+ # @yieldparam [Resource] dep the domain object to visit
443
+ def visit_dependents(&operator) # :yields: dependent
444
+ DEPENDENT_VISITOR.visit(self, &operator)
445
+ end
446
+
447
+ # Applies the operator block to the transitive closure of this domain object's owner relation.
448
+ #
449
+ # @yield [dep] operation on the visited domain object
450
+ # @yieldparam [Resource] dep the domain object to visit
451
+ def visit_owners(&operator) # :yields: owner
452
+ ref = owner
453
+ yield(ref) and ref.visit_owners(&operator) if ref
454
+ end
455
+
456
+ def pretty_print(q)
457
+ q.text(qp)
458
+ content = printable_content
459
+ q.pp_hash(content) unless content.empty?
460
+ end
461
+
462
+ # Prints this domain object's content and recursively prints the referenced content.
463
+ # The optional selector block determines the attributes to print. The default is all
464
+ # Java domain attributes.
465
+ #
466
+ # @yield [owner] the owner attribute selector
467
+ # @yieldparam [Resource] owner the domain object to print
468
+ # @return [String] the domain object content
469
+ def dump(&selector)
470
+ DetailPrinter.new(self, &selector).pp_s
471
+ end
472
+
473
+ # Prints this domain object in the format:
474
+ # class_name@object_id{attribute => value ...}
475
+ # The default attributes include identifying attributes.
476
+ #
477
+ # @param [<Symbol>] attributes the attributes to print
478
+ # @return [String] the formatted content
479
+ def to_s(attributes=nil)
480
+ content = printable_content(attributes)
481
+ content_s = content.pp_s(:single_line) unless content.empty?
482
+ "#{print_class_and_id}#{content_s}"
483
+ end
484
+
485
+ alias :inspect :to_s
486
+
487
+ # Returns this domain object's attributes content as an attribute => value hash
488
+ # suitable for printing.
489
+ #
490
+ # The default attributes are this object's saved attributes. The optional
491
+ # reference_printer is used to print a referenced domain object.
492
+ #
493
+ # @param [<Symbol>, nil] attributes the attributes to print
494
+ # @yield [ref] the reference print formatter
495
+ # @yieldparam [Rresource] ref the referenced domain object to print
496
+ # @return [{Symbol => String}] the attribute => content hash
497
+ def printable_content(attributes=nil, &reference_printer) # :yields: reference
498
+ attributes ||= printworthy_attributes
499
+ vh = suspend_lazy_loader { value_hash(attributes) }
500
+ vh.transform { |value| printable_value(value, &reference_printer) }
501
+ end
502
+
503
+ # Returns whether value equals other modulo the given matches according to the following tests:
504
+ # * _value_ == _other_
505
+ # * _value_ and _other_ are Ruby DateTime instances and _value_ equals _other_,
506
+ # modulo the Java-Ruby DST differences described in the following paragraph
507
+ # * _value_ == matches[_other_]
508
+ # * _value_ and _other_ are Resource instances and _value_ is a {#match?} with _other_.
509
+ # * _value_ and _other_ are Enumerable with members equal according to the above conditions.
510
+ # The DateTime comparison accounts for differences in the Ruby -> Java -> Ruby roundtrip
511
+ # of a date attribute.
512
+ #
513
+ # Ruby alert - Unlike Java, Ruby does not adjust the offset for DST.
514
+ # This adjustment is made in the {Java::JavaUtil::Date#to_ruby_date}
515
+ # and {Java::JavaUtil::Date.from_ruby_date} methods.
516
+ # A date sent to the database is stored correctly.
517
+ # However, a date sent to the database does not correctly compare to
518
+ # the data fetched from the database.
519
+ # This method accounts for the DST discrepancy when comparing dates.
520
+ #
521
+ # @return whether value and other are equal according to the above tests
522
+ def self.value_equal?(value, other, matches=nil)
523
+ if value == other then
524
+ true
525
+ elsif value.collection? and other.collection? then
526
+ collection_value_equal?(value, other, matches)
527
+ elsif DateTime === value and DateTime === other then
528
+ value == other or dates_equal_modulo_dst(value, other)
529
+ elsif Resource === value and value.class === other then
530
+ value.match?(other)
531
+ elsif matches then
532
+ matches[value] == other
533
+ else
534
+ false
535
+ end
536
+ end
537
+
538
+ protected
539
+
540
+ # Adds the default values to this object, if it is not already fetched, and its dependents.
541
+ def add_defaults_recursive
542
+ # add the local defaults unless there is an identifier
543
+ add_defaults_local unless identifier
544
+ # add dependent defaults
545
+ each_defaults_dependent { |dep| dep.add_defaults_recursive }
546
+ end
547
+
548
+ # Returns the required attributes for this domain object which are nil or empty.
549
+ #
550
+ # This method is in protected scope to allow the +CaTissue+ domain module to
551
+ # work around a caTissue bug (see that module for details). Other definitions
552
+ # of this method are discouraged.
553
+ def missing_mandatory_attributes
554
+ self.class.mandatory_attributes.select { |attr| send(attr).nil_or_empty? }
555
+ end
556
+
557
+ private
558
+
559
+ # The copy merge call options.
560
+ COPY_MERGE_OPTS = {:inverse => false}
561
+
562
+ # The dependent attribute visitor.
563
+ #
564
+ # @see #visit_dependents
565
+ DEPENDENT_VISITOR = CaRuby::ReferenceVisitor.new { |obj| obj.class.dependent_attributes }
566
+
567
+ # Matches the given targets to sources using {Resource#match_in}.
568
+ class Matcher
569
+ def match(sources, targets)
570
+ unmatched = Set === sources ? sources.dup : sources.to_set
571
+ matches = {}
572
+ targets.each do |tgt|
573
+ src = tgt.match_in(unmatched)
574
+ if src then
575
+ unmatched.delete(src)
576
+ matches[src] = tgt
577
+ end
578
+ end
579
+ matches
580
+ end
581
+ end
582
+
583
+ DEF_MATCHER = Matcher.new
584
+
585
+ # Sets the default attribute values for this domain object. Unlike {#add_defaults}, this
586
+ # method does not set defaults for dependents. This method sets the configuration values
587
+ # for this domain object as described in {#add_defaults}, but does not set defaults for
588
+ # dependents.
589
+ #
590
+ # This method is the integration point for subclasses to augment defaults with programmatic logic.
591
+ # If a subclass overrides this method, then it should call super before setting the local
592
+ # default attributes. This ensures that configuration defaults takes precedence.
593
+ def add_defaults_local
594
+ merge_attributes(self.class.defaults)
595
+ self
596
+ end
597
+
598
+ # Enumerates the dependents for setting defaults. Subclasses can override if the
599
+ # dependents must be visited in a certain order.
600
+ alias :each_defaults_dependent :each_dependent
601
+
602
+ # @return [Boolean] whether the given key attributes is non-empty and each attribute in the key has a non-nil value
603
+ def key_searchable?(attributes)
604
+ not (attributes.empty? or attributes.any? { |attr| send(attr).nil? })
605
+ end
606
+
607
+ # Returns the source => target hash of matches for the given attr_md newval sources and
608
+ # oldval targets. If the matcher block is given, then that block is called on the sources
609
+ # and targets. Otherwise, {Resource.match_all} is called.
610
+ #
611
+ # @param [AttributeMetadata] attr_md the attribute to match
612
+ # @param newval the source value
613
+ # @param oldval the target value
614
+ # @yield [sources, targets] matches sources to targets
615
+ # @yieldparam [<Resource>] sources an Enumerable on the source value
616
+ # @yieldparam [<Resource>] targets an Enumerable on the target value
617
+ # @return [{Resource => Resource}] the source => target matches
618
+ def match_attribute_value(attr_md, newval, oldval)
619
+ # make Enumerable targets and sources for matching
620
+ sources = newval.to_enum
621
+ targets = oldval.to_enum
622
+
623
+ # match sources to targets
624
+ logger.debug { "Matching source #{newval.qp} to target #{qp} #{attr_md} #{oldval.qp}..." }
625
+ block_given? ? yield(sources, targets) : Resource.match_all(sources, targets)
626
+ end
627
+
628
+ # @param [DateTime] d1 the first date
629
+ # @param [DateTime] d2 the second date
630
+ # @return whether d1 matches d2 on the non-offset fields
631
+ # @see #value_equal
632
+ def self.dates_equal_modulo_dst(d1, d2)
633
+ d1.strftime('%FT%T') == d1.strftime('%FT%T')
634
+ end
635
+
636
+ def self.collection_value_equal?(value, other, matches=nil)
637
+ value.size == other.size and value.all? { |v| other.include?(v) or (matches and other.include?(matches[v])) }
638
+ end
639
+
640
+ # @param (see #merge_match)
641
+ # @return [String] the log message
642
+ def format_merge_log_message(other, attributes)
643
+ attr_list = attributes.sort { |a1, a2| a1.to_s <=> a2.to_s }
644
+ attr_clause = " including domain attributes #{attr_list.to_series}" unless attr_list.empty?
645
+ "Merging #{other.qp} into #{qp}#{attr_clause}..."
646
+ end
647
+
648
+ # A DetailPrinter formats a domain object value for printing using {#to_s} the first time the object
649
+ # is encountered and a ReferencePrinter on the object subsequently.
650
+ class DetailPrinter
651
+ alias :to_s :pp_s
652
+
653
+ alias :inspect :to_s
654
+
655
+ # Creates a DetailPrinter on the base object.
656
+ def initialize(base, visited=Set.new, &selector)
657
+ @base = base
658
+ @visited = visited << base
659
+ @selector = selector || Proc.new { |ref| ref.class.java_attributes }
660
+ end
661
+
662
+ def pretty_print(q)
663
+ q.text(@base.qp)
664
+ # pretty-print the standard attribute values
665
+ attrs = @selector.call(@base)
666
+ content = @base.printable_content(attrs) do |ref|
667
+ @visited.include?(ref) ? ReferencePrinter.new(ref) : DetailPrinter.new(ref, @visited) { |ref| @selector.call(ref) }
668
+ end
669
+ q.pp_hash(content)
670
+ end
671
+ end
672
+
673
+ # A ReferencePrinter formats a reference domain object value for printing with just the class and Ruby object_id.
674
+ class ReferencePrinter
675
+ extend Forwardable
676
+
677
+ def_delegator(:@base, :qp, :to_s)
678
+
679
+ alias :inspect :to_s
680
+
681
+ # Creates a ReferencePrinter on the base object.
682
+ def initialize(base)
683
+ @base = base
684
+ end
685
+ end
686
+
687
+ # Returns a value suitable for printing. If value is a domain object, then the block provided to this method is called.
688
+ # The default block creates a new ReferencePrinter on the value.
689
+ def printable_value(value, &reference_printer)
690
+ Collector.on(value) do |item|
691
+ if Resource === item then
692
+ block_given? ? yield(item) : printable_value(item) { |ref| ReferencePrinter.new(ref) }
693
+ else
694
+ item
695
+ end
696
+ end
697
+ end
698
+
699
+ # Returns an attribute => value hash for the +identifier+ attribute, if there is a non_nil +identifier+,
700
+ # If +identifier+ is nil, then this method returns the secondary key attributes, if they exist,
701
+ # or the mergeable attributes otherwise. If this is a dependent object, then the owner attribute is
702
+ # removed from the returned array.
703
+ def printworthy_attributes
704
+ return self.class.primary_key_attributes if identifier
705
+ attrs = self.class.secondary_key_attributes
706
+ attrs = self.class.nondomain_java_attributes if attrs.empty?
707
+ attrs = self.class.fetched_attributes if attrs.empty?
708
+ attrs
709
+ end
710
+
711
+ # Substitutes attribute with the standard attribute and a Java non-Domain instance value with a Domain object if necessary.
712
+ #
713
+ # Returns the [standard attribute, standard value] array.
714
+ def standardize_attribute_value(attribute, value)
715
+ attr_md = self.class.attribute_metadata(attribute)
716
+ if attr_md.nil? then
717
+ raise ArgumentError.new("#{attribute} is neither a #{self.class.qp} standard attribute nor an alias for a standard attribute")
718
+ end
719
+ # standardize the value if necessary
720
+ std_val = attr_md.type && attr_md.type < Resource ? standardize_domain_value(value) : value
721
+ [attr_md.to_sym, std_val]
722
+ end
723
+
724
+ # Returns a Domain object for a Java non-Domain instance value.
725
+ def standardize_domain_value(value)
726
+ if value.nil? or Resource === value then
727
+ value
728
+ elsif Enumerable === value then
729
+ # value is a collection; if value is a nested collection (highly unlikely), then recursively standarize
730
+ # the value collection members. otherwise, leave the value alone.
731
+ value.empty? || Resource === value.first ? value : value.map { |item| standardize_domain_value(item) }
732
+ else
733
+ # return a new Domain object built from the source Java domain object
734
+ # (unlikely unless value is a weird toxic Hibernate proxy)
735
+ logger.debug { "Creating standard domain object from #{value}..." }
736
+ Domain.const_get(value.class.qp).new.merge_attributes(value)
737
+ end
738
+ end
739
+
740
+ # @see #merge_attribute_value
741
+ def merge_domain_attribute_value(attr_md, oldval, newval, &matcher)
742
+ # the source => target matches
743
+ matches = match_attribute_value(attr_md, newval, oldval, &matcher)
744
+ logger.debug { "Matched #{qp} #{attr_md}: #{matches.qp}." } unless matches.empty?
745
+ merge_matching_attribute_references(attr_md, oldval, newval, matches)
746
+ # return the merged result
747
+ send(attr_md.reader)
748
+ end
749
+
750
+ # @see #merge_attribute_value
751
+ def merge_matching_attribute_references(attr_md, oldval, newval, matches)
752
+ # the dependent owner writer method, if any
753
+ inv_md = attr_md.inverse_attribute_metadata
754
+ if inv_md and not inv_md.collection? then
755
+ owtr = inv_md.writer
756
+ end
757
+
758
+ # if the attribute is a collection, then merge the matches into the current attribute
759
+ # collection value and add each unmatched source to the collection.
760
+ # otherwise, if the attribute is not yet set and there is a new value, then set it
761
+ # to the new value match or the new value itself if unmatched.
762
+ if attr_md.collection? then
763
+ # TODO - refactor into method
764
+ # the references to add
765
+ adds = []
766
+ logger.debug { "Merging #{newval.qp} into #{qp} #{attr_md} #{oldval.qp}..." } unless newval.empty?
767
+ newval.each do |src|
768
+ # if the match target is in the current collection, then update the matched
769
+ # target from the source.
770
+ # otherwise, if there is no match or the match is a new reference created
771
+ # from the match, then add the match to the oldval collection.
772
+ if matches.has_key?(src) then
773
+ # the source match
774
+ tgt = matches[src]
775
+ if oldval.include?(tgt) then
776
+ tgt.merge_attributes(src)
777
+ else
778
+ adds << tgt
779
+ end
780
+ else
781
+ adds << src
782
+ end
783
+ end
784
+ # add the unmatched sources
785
+ logger.debug { "Adding #{qp} #{attr_md} unmatched #{adds.qp}..." } unless adds.empty?
786
+ adds.each do |ref|
787
+ # if there is an owner writer attribute, then add the ref to the attribute collection by
788
+ # delegating to the owner writer. otherwise, add the ref to the attribute collection directly.
789
+ owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : oldval << ref
790
+ end
791
+ elsif newval then
792
+ if oldval then
793
+ oldval.merge(newval)
794
+ else
795
+ # the target is either a new object created in the match or the source value
796
+ ref = matches.has_key?(newval) ? matches[newval] : newval
797
+ # if the target is a dependent, then set the dependent owner, which in turn will
798
+ # set the attribute to the dependent. otherwise, set attribute to the target.
799
+ logger.debug { "Setting #{qp} #{attr_md} to #{ref.qp}..." }
800
+ owtr ? delegate_to_inverse_setter(attr_md, ref, owtr) : send(attr_md.writer, ref)
801
+ end
802
+ end
803
+ end
804
+
805
+ def delegate_to_inverse_setter(attr_md, ref, writer)
806
+ logger.debug { "Setting #{qp} #{attr_md} by setting the #{ref.qp} inverse attribute #{attr_md.inverse}..." }
807
+ ref.send(writer, self)
808
+ end
809
+
810
+ # Sets an exclusive dependent attribute to the given dependent dep.
811
+ # If dep is not nil, then this method calls the dep inv_writer with argument self
812
+ # before calling the writer with argument dep.
813
+ def set_exclusive_dependent(dep, writer, inv_writer)
814
+ dep.send(inv_writer, self) if dep
815
+ send(writer, dep)
816
+ end
817
+
818
+ # Sets the inversible attribute with the given accessors and inverse to
819
+ # the given newval. The inverse of the attribute is a collection accessed by
820
+ # calling inverse on newval.
821
+ #
822
+ # For an attribute +owner+ with writer +setOwner+ and inverse reader +deps+,
823
+ # this is equivalent to the following:
824
+ # class Dependent
825
+ # def owner=(o)
826
+ # owner.deps.delete(owner) if owner
827
+ # setOwner(o)
828
+ # o.deps << self if o
829
+ # end
830
+ # end
831
+ #
832
+ # @param [Resource] new_ref the new attribute reference value
833
+ # @param [(Symbol, Symbol)] accessors the reader and writer to use in setting
834
+ # the attribute
835
+ # @param [Symbol] inverse the inverse collection attribute to which
836
+ # this domain object will be added
837
+ def add_to_inverse_collection(new_ref, accessors, inverse)
838
+ reader, writer = accessors
839
+ # the current inverse
840
+ old_ref = send(reader)
841
+ # no-op if no change
842
+ return new_ref if old_ref == new_ref
843
+
844
+ # delete self from the current inverse reference collection
845
+ if old_ref then
846
+ old_ref.suspend_lazy_loader do
847
+ oldcoll = old_ref.send(inverse)
848
+ oldcoll.delete(self) if oldcoll
849
+ end
850
+ end
851
+
852
+ # call the writer on this object
853
+ send(writer, new_ref)
854
+ # add self to the inverse collection
855
+ if new_ref then
856
+ new_ref.suspend_lazy_loader do
857
+ newcoll = new_ref.send(inverse)
858
+ newcoll << self
859
+ unless newcoll then
860
+ raise TypeError.new("Cannot create #{new_ref.qp} #{inverse} collection to hold #{self}")
861
+ end
862
+ if old_ref then
863
+ logger.debug { "Moved #{qp} from #{reader} #{old_ref.qp} #{inverse} to #{new_ref.qp}." }
864
+ else
865
+ logger.debug { "Added #{qp} to #{reader} #{new_ref.qp} #{inverse}." }
866
+ end
867
+ end
868
+ end
869
+ new_ref
870
+ end
871
+
872
+ # Sets the attribute with the given accessors and inverse_writer to the given newval.
873
+ #
874
+ # For an attribute +owner+ with writer +setOwner+ and inverse +dep+, this is equivalent
875
+ # to the following:
876
+ # class Dependent
877
+ # def owner=(o)
878
+ # owner.dep = nil if owner
879
+ # setOwner(o)
880
+ # o.dep = self if o
881
+ # end
882
+ # end
883
+ def set_inversible_noncollection_attribute(newval, accessors, inverse_writer)
884
+ reader, writer = accessors
885
+ # the previous value
886
+ oldval = suspend_lazy_loader { send(reader) }
887
+ # bail if no change
888
+ return newval if newval.equal?(oldval)
889
+
890
+ # clear the previous inverse
891
+ oldval.send(inverse_writer, nil) if oldval
892
+ # call the writer
893
+ send(writer, newval)
894
+ logger.debug { "Moved #{qp} from #{oldval.qp} to #{newval.qp}." } if oldval and newval
895
+ # call the inverse writer on self
896
+ newval.send(inverse_writer, self) if newval
897
+ newval
898
+ end
899
+
900
+ # Returns 0 if attribute is a Java primitive number,
901
+ # +false+ if attribute is a Java primitive boolean, nil otherwise.
902
+ def empty_value(attribute)
903
+ type = java_type(attribute)
904
+ return unless type and type.primitive?
905
+ type.name == 'boolean' ? false : 0
906
+ end
907
+
908
+ # Returns the Java type of the given attribute, or nil if attribute is not a Java property attribute.
909
+ def java_type(attribute)
910
+ attr_md = self.class.attribute_metadata(attribute)
911
+ attr_md.property_descriptor.property_type if JavaAttributeMetadata === attr_md
912
+ end
913
+
914
+ # Returns the object in others which uniquely matches this domain object on the given attributes,
915
+ # or nil if there is no unique match. This method returns nil if any attributes value is nil.
916
+ def match_unique_object_with_attributes(others, attributes)
917
+ vh = value_hash(attributes)
918
+ return if vh.empty? or vh.size < attributes.size
919
+ matches = match_attribute_values(others, vh)
920
+ matches.first if matches.size == 1
921
+ end
922
+
923
+ # Returns the domain objects in others whose class is the same as this object's class
924
+ # and whose attribute values equal those in the given attr_value_hash.
925
+ def match_attribute_values(others, attr_value_hash)
926
+ others.select do |other|
927
+ self.class === other and attr_value_hash.all? do |attr, value|
928
+ matches_attribute_value?(other, attr, value)
929
+ end
930
+ end
931
+ end
932
+
933
+ # Returns whether this Resource's attribute value matches the fetched other attribute.
934
+ # A domain attribute match is determined by {#match?}.
935
+ # A non-domain attribute match is determined by an equality comparison.
936
+ def matches_attribute_value?(other, attribute, value)
937
+ other_val = other.send(attribute)
938
+ if Resource === value then
939
+ value.match?(other_val)
940
+ else
941
+ value == other_val
942
+ end
943
+ end
944
+
945
+ # Returns the attribute => value hash to use for matching this domain object as follows:
946
+ # * If this domain object has a database identifier, then the identifier is the sole match criterion attribute.
947
+ # * Otherwise, if a secondary key is defined for the object's class, then those attributes are used.
948
+ # * Otherwise, all attributes are used.
949
+ #
950
+ # If any secondary key value is nil, then this method returns an empty hash, since the search is ambiguous.
951
+ def search_attribute_values
952
+ # if this object has a database identifier, then the identifier is the search criterion
953
+ identifier.nil? ? non_id_search_attribute_values : { :identifier => identifier }
954
+ end
955
+
956
+ # Returns the attribute => value hash to use for matching this domain object.
957
+ # @see #search_attribute_values the method specification
958
+ def non_id_search_attribute_values
959
+ # if there is a secondary key, then search on those attributes.
960
+ # otherwise, search on all attributes.
961
+ key_attrs = self.class.secondary_key_attributes
962
+ attrs = key_attrs.empty? ? self.class.nondomain_java_attributes : key_attrs
963
+ # associate the values
964
+ attr_values = attrs.to_compact_hash { |attr| send(attr) }
965
+ # if there is no secondary key, then cull empty values
966
+ key_attrs.empty? ? attr_values.delete_if { |attr, value| value.nil? } : attr_values
967
+ end
968
+ end
969
+ end