caruby-core 1.4.1

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