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,162 @@
1
+ require 'caruby/util/version'
2
+ require 'caruby/database'
3
+ require 'caruby/util/stopwatch'
4
+
5
+ import 'gov.nih.nci.common.util.HQLCriteria'
6
+
7
+ module CaRuby
8
+ # A PersistenceService wraps a caCORE application service.
9
+ class PersistenceService
10
+ # The service name.
11
+ attr_reader :name
12
+
13
+ # The {Util::Stopwatch} which captures the time spent in database operations performed by the application service.
14
+ attr_reader :timer
15
+
16
+ # Creates a new PersistenceService with the specified application service name.
17
+ def initialize(name, opts={})
18
+ @name = name
19
+ ver_opt = opts[:version]
20
+ @version = ver_opt.to_s.to_version if ver_opt
21
+ @host = opts[:host] || "localhost"
22
+ @timer = Stopwatch.new
23
+ end
24
+
25
+ ## Database access methods ##
26
+
27
+ # Returns an array of objects fetched from the database which match the given template_or_hql.
28
+ #
29
+ # If template_or_hql is a String, then the HQL is submitted to the service.
30
+ #
31
+ # Otherwise, the template_or_hql is a query template domain
32
+ # object following the given attribute path. The query condition is determined by the values set in the
33
+ # template. Every non-nil attribute in the template is used as a select condition.
34
+ #
35
+ # caCORE alert - this method returns the direct result of calling the +caCORE+ application service
36
+ # search method. Calling reference attributes of this result is broken by +caCORE+ design.
37
+ def query(template_or_hql, *path)
38
+ String === template_or_hql ? query_hql(template_or_hql) : query_template(template_or_hql, path)
39
+ end
40
+
41
+ # Submits the create to the application service and returns the created object.
42
+ #
43
+ # caCORE alert - this method returns the direct result of calling the +caCORE+ application service
44
+ # create method. Calling reference attributes of this result is broken by +caCORE+ design.
45
+ def create(obj)
46
+ logger.debug { "Submitting create #{obj.pp_s(:single_line)} to application service #{name}..." }
47
+ begin
48
+ dispatch { |svc| svc.create_object(obj) }
49
+ rescue Exception => e
50
+ logger.error("Error creating #{obj} - #{e.message}\n#{dump(obj)}")
51
+ raise
52
+ end
53
+ end
54
+
55
+ # Submits the update to the application service and returns obj.
56
+ def update(obj)
57
+ logger.debug { "Submitting update #{obj.pp_s(:single_line)} to application service #{name}..." }
58
+ begin
59
+ dispatch { |svc| svc.update_object(obj) }
60
+ rescue Exception => e
61
+ logger.error("Error updating #{obj} - #{e.message}\n#{dump(obj)}")
62
+ raise
63
+ end
64
+ end
65
+
66
+ # Submits the delete to the application service.
67
+ def delete(obj)
68
+ logger.debug { 'Deleting #{obj}.' }
69
+ begin
70
+ dispatch { |svc| svc.remove_object(obj) }
71
+ rescue Exception => e
72
+ logger.error("Error deleting #{obj} - #{e.message}\n#{dump(obj)}")
73
+ raise
74
+ end
75
+ end
76
+
77
+ # Returns the CaCORE ApplicationServiceProvider wrapped by this PersistenceService.
78
+ def app_service
79
+ url = "http://#{@host}:8080/#{name}/http/remoteService"
80
+ logger.debug { "Connecting to service provider at #{url}..." }
81
+ ApplicationServiceProvider.remote_instance(url)
82
+ end
83
+
84
+ private
85
+
86
+ # The first caCORE Version which supports association search.
87
+ ASSOCIATION_SUPPORT_VERSION = "4".to_version
88
+
89
+ # Calls the block given to this method. The execution duration is captured in the {#timer}.
90
+ # Returns the block result.
91
+ def dispatch
92
+ result = nil
93
+ seconds = @timer.run { result = yield app_service }.elapsed
94
+ millis = (seconds * 1000).round
95
+ logger.debug { "Database operation took #{millis} milliseconds." }
96
+ result
97
+ end
98
+
99
+ def query_hql(hql)
100
+ logger.debug { "Building HQLCriteria..." }
101
+ criteria = HQLCriteria.new(hql)
102
+ # caCORE alert - query target parameter is necessary for caCORE 3.x but deprecated in caCORE 4+
103
+ # TODO caCORE 4 - remove target parameter
104
+ target = hql[/from\s+(\S+)/i, 1]
105
+ raise DatabaseError.new("HQL does not contain a FROM clause: #{hql}") unless target
106
+ logger.debug { "Calling application service query on target class #{target} with following HQL:\n#{hql}" }
107
+ begin
108
+ dispatch { |svc| svc.query(criteria, target) }
109
+ rescue Exception => e
110
+ logger.error("Error querying on HQL - #{$!}:\n#{hql}")
111
+ raise
112
+ end
113
+ end
114
+
115
+ def query_template(template, path)
116
+ if path.length == 1 and template.identifier and @version and @version >= ASSOCIATION_SUPPORT_VERSION then
117
+ return query_association_post_caCORE_v4(template, path.first)
118
+ end
119
+ logger.debug { "Searching using template #{template.qp}#{', path ' + path.join('.') unless path.empty?}..." }
120
+ # collect the class search path from the reference attribute domain type Java class names
121
+ class_name_path = []
122
+ path.inject(template.class) do |type, attr|
123
+ ref_type = type.domain_type(attr)
124
+ raise DatabaseError.new("Attribute in search attribute path #{path.join('.')} is not a #{type} domain reference attribute: #{attr}") if ref_type.nil?
125
+ class_name_path << ref_type.java_class.name
126
+ ref_type
127
+ end
128
+ # the caCORE app service search path is in reverse path traversal order (go figure!)
129
+ reverse_class_name_path = class_name_path.reverse << template.java_class
130
+ # call the caCORE app service search
131
+ logger.debug { "Calling application service search with template #{template.qp}, target-first class path #{reverse_class_name_path.pp_s(:single_line)}, criterion:\n#{dump(template)}" }
132
+ begin
133
+ dispatch { |svc| svc.search(reverse_class_name_path.join(','), template) }
134
+ rescue Exception => e
135
+ logger.error("Error searching on template #{template}#{', path ' + path.join('.') unless path.empty?} - #{$!}\n#{dump(template)}")
136
+ raise
137
+ end
138
+ end
139
+
140
+ # Returns an array of domain objects associated with obj through the specified attribute.
141
+ # This method uses the +caCORE+ v. 4+ getAssociation application service method.
142
+ #
143
+ # *Note*: this method is only available for caBIG application services which implement +getAssociation+.
144
+ # Currently, this includes +caCORE+ v. 4.0 and above.
145
+ # This method raises a DatabaseError if the application service does not implement +getAssociation+.
146
+ #
147
+ # Raises DatabaseError if the attribute is not an a domain attribute or the associated objects were not fetched.
148
+ def query_association_post_caCORE_v4(obj, attribute)
149
+ assn = obj.class.association(attribute)
150
+ begin
151
+ result = dispatch { |svc| svc.association(obj, assn) }
152
+ rescue Exception => e
153
+ logger.error("Error fetching association #{obj} - #{e.message}\n#{dump(obj)}")
154
+ raise
155
+ end
156
+ end
157
+
158
+ def dump(obj)
159
+ Resource === obj ? obj.dump : obj.to_s
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,599 @@
1
+ require 'caruby/util/collection'
2
+ require 'caruby/util/cache'
3
+ require 'caruby/util/pretty_print'
4
+ require 'caruby/domain/reference_visitor'
5
+ require 'caruby/database/fetched_matcher'
6
+ require 'caruby/database/search_template_builder'
7
+
8
+ module CaRuby
9
+ class Database
10
+ # Database query operation mixin.
11
+ module Reader
12
+ # Adds query capability to this Database.
13
+ def initialize
14
+ super
15
+ # the demand loader
16
+ @lazy_loader = lambda { |obj, attr| lazy_load(obj, attr) }
17
+ # the query template builder
18
+ @srch_tmpl_bldr = SearchTemplateBuilder.new(self)
19
+ # the fetch result matcher
20
+ @matcher = FetchedMatcher.new
21
+
22
+ # cache not yet tested - TODO: test and replace copier below with cacher
23
+ # the fetched object cacher
24
+ #cacher = Proc.new { |src| @cache[src] }
25
+
26
+ # the fetched copier
27
+ copier = Proc.new do |src|
28
+ copy = src.copy
29
+ logger.debug { "Fetched #{src.qp} copied to #{copy.qp}." }
30
+ copy
31
+ end
32
+ # visitor that merges the fetched object graph
33
+ @ftchd_vstr = ReferenceVisitor.new { |tgt| tgt.class.fetched_domain_attributes }
34
+ @ftchd_mrg_vstr = MergeVisitor.new(:matcher => @matcher, :copier => copier) { |src, tgt| tgt.class.fetched_domain_attributes }
35
+ # visitor that copies the fetched object graph
36
+ @detoxifier = CopyVisitor.new(:copier => copier) { |src, tgt| src.class.fetched_domain_attributes }
37
+ end
38
+
39
+ # Returns an array of objects matching the specified query template and attribute path.
40
+ # The obj_or_hql argument is either a domain object template or a
41
+ # Hibernate[http://www.hibernate.org/docs.html] HQL statement. If obj_or_hql
42
+ # is a String, then the HQL statement String is executed.
43
+ #
44
+ # Otherwise, the query condition is determined by the values set in the template.
45
+ # The non-nil {ResourceAttributes#searchable_attributes} are used in the query.
46
+ #
47
+ # The optional path arguments are attribute symbols from the template to the
48
+ # destination class, e.g.:
49
+ # query(study, :registration, :participant)
50
+ # returns study registration participants.
51
+ #
52
+ # Unlike caCORE, the query result reflects the database state, i.e. calling an attribute
53
+ # accessor method on a query result object returns the database value, e.g.:
54
+ # query(study, :registration).first.participant
55
+ # has the same content as:
56
+ # query(study, :registration, :participant).first
57
+ #
58
+ # By contrast, caCORE API search result property access, by design, fails with an
59
+ # obscure exception when the property is not lazy-loaded in Hibernate.
60
+ #
61
+ # @param [Resource, String] obj_or_hql the domain object or HQL to query
62
+ # @param [<Attribute>] path the attribute path to search
63
+ # @return [<Resource>] the domain objects which match the query
64
+ def query(obj_or_hql, *path)
65
+ # the detoxified caCORE query result
66
+ result = query_safe(obj_or_hql, *path)
67
+ # enable change tracking and lazy-loading
68
+ persistify(result)
69
+ end
70
+
71
+ # Queries the given obj_or_hql as described in {#query} and makes a detoxified copy of the
72
+ # toxic caCORE search result.
73
+ #
74
+ # caCORE alert - The query result consists of new domain objects whose content is copied
75
+ # from the caBIG application search result. The caBIG result is Hibernate-enhanced but
76
+ # sessionless. This result contains toxic broken objects whose access methods fail.
77
+ # Therefore, this method sanitizes the toxic caBIG result to reflect the persistent state
78
+ # of the domain objects. Persistent references are loaded on demand from the database if
79
+ # necessary.
80
+ #
81
+ # @param (see #query)
82
+ # @return (see #query)
83
+ def query_safe(obj_or_hql, *path)
84
+ # the caCORE search result
85
+ toxic = query_toxic(obj_or_hql, *path)
86
+ logger.debug { "Copying caCORE query toxic #{toxic.qp}..." } unless toxic.empty?
87
+ # detoxify the toxic caCORE result
88
+ detoxify(toxic)
89
+ end
90
+
91
+ # Queries the given obj_or_hql as described in {#query} and returns the toxic caCORE search result.
92
+ #
93
+ # @param (see #query)
94
+ # @return (see #query)
95
+ def query_toxic(obj_or_hql, *path)
96
+ # the attribute path as a string
97
+ path_s = path.join('.') unless path.empty?
98
+ # guard against recursive call back into the same operation
99
+ if query_redundant?(obj_or_hql, path_s) then
100
+ raise DatabaseError.new("Query #{obj_or_hql.qp} #{path_s} recursively called in context #{print_operations}")
101
+ end
102
+ # perform the query
103
+ perform(:query, obj_or_hql, path_s) { query_with_path(obj_or_hql, path) }
104
+ end
105
+
106
+ # Fetches the given domain object from the database.
107
+ # Only secondary key attributes are used in the match.
108
+ # If no secondary key is defined for the object's class, then this method returns nil.
109
+ # The {#query} method is used to fetch records on non-secondary key attributes.
110
+ #
111
+ # If the :create option is set, then this method creates an object if the
112
+ # find is unsuccessful.
113
+ #
114
+ # @param [Resource] obj the domain object to find
115
+ # @param [Hash, Symbol] opts the find options
116
+ # @option opts [Boolean] :create whether to create the object if it is not found
117
+ # @return [Resource, nil] the domain object if found, nil otherwise
118
+ # @raise [DatabaseError] if obj is not a domain object or more than object
119
+ # matches the obj attribute values
120
+ def find(obj, opts=nil)
121
+ return if obj.nil?
122
+ perform(:find, obj) do
123
+ if find_object(obj) then
124
+ logger.info { "Found #{obj}." }
125
+ obj
126
+ else
127
+ logger.info { "#{obj.qp} not found." }
128
+ if Options.get(:create, opts) then create(obj) end
129
+ end
130
+ end
131
+ end
132
+
133
+ # Returns whether domain object obj has a database identifier or exists in the database.
134
+ # This method fetches obj from the database if necessary.
135
+ # If obj is a domain object collection, then returns whether each item in the collection exists.
136
+ def exists?(obj)
137
+ if obj.nil? then
138
+ false
139
+ elsif obj.collection? then
140
+ obj.all? { |item| exists?(item) }
141
+ else
142
+ obj.identifier or find(obj)
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ RESULT_PRINTER = PrintWrapper.new { |obj| obj.qp }
149
+
150
+ def lazy_load(obj, attribute)
151
+ value = fetch_association(obj, attribute)
152
+ # add a lazy loader and snapshot to each fetched reference
153
+ persistify(value) if value
154
+ end
155
+
156
+ def query_redundant?(obj_or_hql, path)
157
+ @operations.detect { |op| op.type == :query and query_subject_redundant?(op.subject, obj_or_hql) and op.attribute == path }
158
+ end
159
+
160
+ def query_subject_redundant?(s1, s2)
161
+ s1 == s2 or (Resource === s1 and Resource === s2 and s1.identifier and s1.identifier == s2.identifier)
162
+ end
163
+
164
+ # @return an array of objects matching the given query template and path
165
+ # @see #query
166
+ def query_with_path(obj_or_hql, path)
167
+ # the last attribute in the path, if any
168
+ attribute = path.pop
169
+ # if there is more than attribute to follow, then query up to the last attribute and
170
+ # gather the results of querying on those penultimate result objects with the last
171
+ # attribute as the path
172
+ unless path.empty? then
173
+ if attribute.nil? then raise DatabaseError.new("Query path includes empty attribute: #{path.join('.')}.nil") end
174
+ logger.debug { "Decomposing query on #{obj_or_hql} with path #{path.join('.')}.#{attribute} into query on #{path.join('.')} followed by #{attribute}..." }
175
+ return query_safe(obj_or_hql, *path).map { |parent| query_toxic(parent, attribute) }.flatten
176
+ end
177
+ # perform the attribute query
178
+ query_with_attribute(obj_or_hql, attribute)
179
+ end
180
+
181
+ # Returns an array of objects matching the given query template and optional attribute.
182
+ # @see #query
183
+ def query_with_attribute(obj_or_hql, attribute=nil)
184
+ toxic = if String === obj_or_hql then
185
+ hql = obj_or_hql
186
+ # if there is an attribute, then compose the hql query with an attribute query
187
+ if attribute then
188
+ query_safe(hql).map { |parent| query_toxic(parent, attribute) }.flatten
189
+ else
190
+ query_hql(hql)
191
+ end
192
+ else
193
+ obj = obj_or_hql
194
+ query_object(obj, attribute)
195
+ end
196
+ logger.debug { print_query_result(toxic) }
197
+ toxic
198
+ end
199
+
200
+ # caCORE alert - post-process the +caCORE+ search result to fix the following problem:
201
+ # * de-referencing a search result domain object raises a Hibernate missing session error
202
+ #
203
+ # caCORE alert - The caCORE search result does not set the obvious inverse attributes,
204
+ # e.g. the children fetched with a parent do not have the children inverse parent attribute
205
+ # set to the parent. Rather, it is a toxic caCORE reference which must be purged. This
206
+ # leaves an empty reference which must be lazy-loaded, which is inefficient and inconsistent.
207
+ # This situation is rectified in this detoxify method by setting the dependent owner
208
+ # attribute to the fetched owner in the detoxification {ReferenceVisitor} copy-match-merge.
209
+ #
210
+ # This method copies each result domain object into a new object of the same type.
211
+ # The copy nondomain attribute values are set to the fetched object values.
212
+ # The copy fetched reference attribute values are set to a copy of the result references.
213
+ #
214
+ # Returns the detoxified copy.
215
+ def detoxify(toxic)
216
+ return toxic.map { |obj| detoxify(obj) } if toxic.collection?
217
+ @detoxifier.visit(toxic)
218
+ end
219
+
220
+ # Merges fetched into target. The fetched references are recursively merged.
221
+ #
222
+ # @param [Resource] target the domain object find argument
223
+ # @param [Resource] source the fetched domain object result
224
+ def merge_fetched(target, source)
225
+ @ftchd_mrg_vstr.visit(target, source) { |src, tgt| tgt.copy_volatile_attributes(src) }
226
+ end
227
+
228
+ def print_query_result(result)
229
+ count_s = 'result object'.quantify(result.size)
230
+ result_printer = result.wrap { |item| RESULT_PRINTER.wrap(item) }
231
+ "Persistence service query returned #{count_s}: #{result_printer.pp_s(:single_line)}"
232
+ end
233
+
234
+ def query_hql(hql)
235
+ java_name = hql[/from\s+(\S+)/i, 1]
236
+ raise DatabaseError.new("Could not determine target type from HQL: #{hql}") if java_name.nil?
237
+ target = Class.to_ruby(java_name)
238
+ service = persistence_service(target)
239
+ service.query(hql)
240
+ end
241
+
242
+ # Returns an array of objects fetched from the database which matches
243
+ # a template and follows the given optional domain attribute, if present.
244
+ #
245
+ # The search template is built by {SearchTemplateBuilder#build_template}.
246
+ # If a template could not be built and obj is dependent, then this method
247
+ # queries the obj owner with a dependent filter.
248
+ #
249
+ # caCORE alert - Bug #79 - API search with only id returns entire table.
250
+ # Work around this bug by issuing a HQL query instead.
251
+ #
252
+ # @param [Resource] obj the query template object
253
+ # @param [Symbol, nil] attribute the optional attribute to fetch
254
+ # @return [<Resource>] the query result
255
+ def query_object(obj, attribute=nil)
256
+ if invertible_query?(obj, attribute) then
257
+ # caCORE alert - search with attribute ignores id (cf. caTissue Bug #79);
258
+ # inverted query is safer if possible
259
+ query_with_inverted_reference(obj, attribute)
260
+ elsif obj.identifier then
261
+ query_on_identifier(obj, attribute)
262
+ else
263
+ tmpl = @srch_tmpl_bldr.build_template(obj)
264
+ return Array::EMPTY_ARRAY if tmpl.nil?
265
+ query_on_template(tmpl, attribute)
266
+ end
267
+ end
268
+
269
+ # Returns an array of objects fetched from the database which matches
270
+ # the given template and follows the given optional domain attribute.
271
+ def query_on_template(template, attribute=nil)
272
+ target = attribute ? template.class.domain_type(attribute) : template.class
273
+ service = persistence_service(target)
274
+ attribute ? service.query(template, attribute) : service.query(template)
275
+ end
276
+
277
+ # Queries the given obj and attribute by issuing a HQL query with an identifier condition.
278
+ def query_on_identifier(obj, attribute)
279
+ # the source class
280
+ source = obj.class.java_class.name
281
+ # the source alias is the lower-case first letter of the source class name without package prefix
282
+ sa = source[/([[:alnum:]])[[:alnum:]]*$/, 1].downcase
283
+ # the HQL condition
284
+ hql = "from #{source} #{sa} where #{sa}.id = #{obj.identifier}"
285
+
286
+ # the join attribute property
287
+ if attribute then
288
+ pd = obj.class.attribute_metadata(attribute).property_descriptor
289
+ hql.insert(0, "select #{sa}.#{pd.name} ")
290
+ end
291
+ logger.debug { "Querying on #{obj.qp} #{attribute} using HQL #{hql}..." }
292
+
293
+ query_hql(hql)
294
+ end
295
+
296
+ # Returns whether the query specified by obj and attribute can be inverted as a query
297
+ # on a template of type attribute which references obj. This condition holds if obj
298
+ # has a key and attribute is a non-abstract reference with a searchable inverse.
299
+ def invertible_query?(obj, attribute)
300
+ return false if attribute.nil?
301
+ attr_md = obj.class.attribute_metadata(attribute)
302
+ return false if attr_md.type.abstract?
303
+ inv_md = attr_md.inverse_attribute_metadata
304
+ inv_md and inv_md.searchable? and finder_parameters(obj)
305
+ end
306
+
307
+ # Queries the given obj attribute by querying an attribute type template which references obj.
308
+ def query_with_inverted_reference(obj, attribute)
309
+ attr_md = obj.class.attribute_metadata(attribute)
310
+ logger.debug { "Querying on #{obj.qp} #{attribute} by inverting the query as a #{attr_md.type.qp} #{attr_md.inverse} reference query..." }
311
+ # an obj template
312
+ ref = finder_template(obj)
313
+ # the attribute inverse query template
314
+ tmpl = attr_md.type.new
315
+ # the inverse attribute
316
+ inv_md = tmpl.class.attribute_metadata(attr_md.inverse)
317
+ # the Java property writer to set the tmpl inverse to ref.
318
+ # use the property writer rather than the attribute writer in order to curtail automatically
319
+ # adding tmpl to the ref attribute value when the inv_md attribute is set to ref.
320
+ # caCORE alert - caCORE query relies on a lack of inverse integrity, since caCORE search
321
+ # enters an infinite loop upon encountering an object graph cycle.
322
+ writer = inv_md.property_accessors.last
323
+ # parameterize tmpl with inverse ref
324
+ tmpl.send(writer, ref)
325
+ # submit the query
326
+ logger.debug { "Submitting #{obj.qp} #{attribute} inverted query template #{tmpl.qp} ..." }
327
+ persistence_service(tmpl).query(tmpl)
328
+ end
329
+
330
+ # Finds the object matching the specified object obj from the database and merges
331
+ # the matching database values into obj. The find uses the obj secondary or
332
+ # alternate key for the search.
333
+ #
334
+ # Returns nil if obj does not have a complete secondary or alternate key or if
335
+ # there is no matching database object.
336
+ #
337
+ # If a match is found, then each missing obj non-domain-valued attribute is set to the
338
+ # fetched attribute value and this method returns obj.
339
+ #
340
+ # Raises DatabaseError if more than object matches the obj attribute values or if
341
+ # obj is a dependent entity that does not reference an owner.
342
+ def find_object(obj)
343
+ if @transients.include?(obj) then
344
+ logger.debug { "Find #{obj.qp} obviated since the search was previously unsuccessful in the current database operation context." }
345
+ return
346
+ end
347
+ @transients << obj
348
+
349
+ logger.debug { "Fetching #{obj.qp} from the database..." }
350
+ fetched = fetch_object(obj) || return
351
+ # fetch_object can return obj; if so, then done
352
+ return obj if obj.equal?(fetched)
353
+ logger.debug { "Fetch #{obj.qp} matched database object #{fetched}." }
354
+ @transients.delete(obj)
355
+
356
+ # caCORE alert - there is no caCORE find utility method to update a search target with persistent content,
357
+ # so it is done manually here.
358
+ # recursively copy the nondomain attributes, esp. the identifer, of the fetched domain object references
359
+ merge_fetched(obj, fetched)
360
+
361
+ # caCORE alert - see query method alerts
362
+ # inject the lazy loader for loadable domain reference attributes
363
+ persistify(obj, fetched)
364
+ obj
365
+ end
366
+
367
+ # Fetches the object matching the specified object obj from the database.
368
+ #
369
+ # @see #find_object
370
+ def fetch_object(obj)
371
+ # make the finder template with key attributes
372
+ tmpl = finder_template(obj)
373
+ # If a template could be made, then fetch on the template.
374
+ # Otherwise, if there is an owner, then match on the fetched owner dependents.
375
+ if tmpl then
376
+ fetch_object_with_template(obj, tmpl)
377
+ else
378
+ fetch_object_by_fetching_owner(obj)
379
+ end
380
+ end
381
+
382
+ # Fetches the object obj using the given template.
383
+ def fetch_object_with_template(obj, template)
384
+ # submit the query on the template
385
+ logger.debug { "Query template for finding #{obj.qp}: #{template}." }
386
+ result = query_on_template(template)
387
+
388
+ # a fetch query which returns more than one result is an error.
389
+ # possible cause is an incorrect secondary key.
390
+ if result.size > 1 then
391
+ # caCORE alert - annotations are not easily searchable; allow but bail out
392
+ # TODO Annotation - always disable annotation find?
393
+ # if CaRuby::Annotation === obj then
394
+ # logger.debug { "Annotation #{obj} search unsuccessful with template #{template}." }
395
+ # return
396
+ # end
397
+ msg = "More than one match for #{obj.class.qp} find with template #{template}."
398
+ # it is an error to have an ambiguous result
399
+ logger.error("Fetch error - #{msg}:\n#{obj.dump}")
400
+ raise DatabaseError.new(msg)
401
+ end
402
+
403
+ result.first
404
+ end
405
+
406
+ # If obj is a dependent, then returns the obj owner dependent which matches obj.
407
+ # Otherwise, returns nil.
408
+ def fetch_object_by_fetching_owner(obj)
409
+ owner = nil
410
+ oattr = obj.class.owner_attributes.detect { |attr| owner = obj.send(attr) }
411
+ return unless owner
412
+
413
+ logger.debug { "Querying #{obj.qp} by matching on the fetched owner #{owner.qp} #{oattr} dependents..." }
414
+ inverse = obj.class.attribute_metadata(oattr).inverse
415
+ if inverse.nil? then
416
+ raise DatabaseError.new("#{dep.class.qp} owner attribute #{oattr} does not have a #{obj.class.qp} inverse attribute")
417
+ end
418
+ # fetch the owner if necessary
419
+ unless owner.identifier then
420
+ find(owner) || return
421
+ # if obj dependent was fetched with owner, then done
422
+ return obj if obj.identifier
423
+ end
424
+
425
+ deps = query(owner, inverse)
426
+ logger.debug { "Owner #{owner.qp} has #{deps.size} #{inverse} dependents: #{deps.qp}." }
427
+ # If the dependent can be unambiguously matched to one of the results,
428
+ # then return the matching result.
429
+ obj.match_in_owner_scope(deps) unless deps.empty?
430
+ end
431
+
432
+ # Returns a copy of obj containing only those key attributes used in a find operation.
433
+ #
434
+ # caCORE alert - Bug #79: caCORE search fetches on all non-nil attributes, except
435
+ # occasionally the identifier. There is no indication of how to identify uniquely
436
+ # searchable attributes, so the secondary and alternate key is added manually in the
437
+ # application configuration.
438
+ def finder_template(obj)
439
+ hash = finder_parameters(obj) || return
440
+ @srch_tmpl_bldr.build_template(obj, hash)
441
+ end
442
+
443
+ # Fetches the given obj attribute from the database.
444
+ # caCORE alert - there is no association fetch for caCORE 3.1 and earlier;
445
+ # caCORE 4 association search is not yet adequately proven in caRuby testing.
446
+ # Fall back on a general query instead (the devil we know). See also the
447
+ # following alert.
448
+ #
449
+ # caCORE alert - caCORE search on a non-collection attribute returns a collection result,
450
+ # even with the caCORE 4 association search. caRuby rectifies this by returning
451
+ # an association fetch result consistent with the association attribute return type.
452
+ #
453
+ # caCORE alert - Preliminary indication is that caCORE 4 does not validate that
454
+ # a non-collection association search returns at most one item.
455
+ #
456
+ # caCORE alert - Since the caCORE search result has toxic references which must be purged,
457
+ # the detoxified copy loses reference integrity. E.g. a query on the children attribute of
458
+ # a parent object forces lazy load of each child => parent reference separately resolving
459
+ # in separate parent copies. There is no recognition that the children reference the parent
460
+ # which generated the query. This anomaly is partially rectified in this fetch_association
461
+ # method by setting the fetched objects inverse to the given search target object. The
462
+ # inconsistent and inefficient caCORE behavior is further corrected by setting dependent
463
+ # owners in the fetch result, as described in {#query_safe}.
464
+ #
465
+ # @param [Resource] obj the search target object
466
+ # @param [Symbol] attribute the association to fetch
467
+ # @raise [DatabaseError] if the search target object does not have an identifier
468
+ def fetch_association(obj, attribute)
469
+ logger.debug { "Fetching association #{attribute} for #{obj.qp}..." }
470
+ # load the object if necessary
471
+ unless exists?(obj) then
472
+ raise DatabaseError.new("Can't fetch an association since the referencing object is not found in the database: #{obj}")
473
+ end
474
+ # fetch the reference
475
+ result = query_safe(obj, attribute)
476
+ # set inverse references
477
+ inv_md = obj.class.attribute_metadata(attribute).inverse_attribute_metadata
478
+ if inv_md and not inv_md.collection? then
479
+ inv_obj = obj.copy
480
+ result.each do |ref|
481
+ logger.debug { "Setting fetched #{obj} #{attribute} inverse #{inv_md} to #{obj.qp} copy #{inv_obj.qp}..." }
482
+ ref.send(inv_md.writer, inv_obj)
483
+ end
484
+ end
485
+ # unbracket the result if the attribute is not a collection
486
+ obj.class.attribute_metadata(attribute).collection? ? result : result.first
487
+ end
488
+
489
+ # Returns a copy of obj containing only those key attributes used in a find operation.
490
+ #
491
+ # caCORE alert - caCORE search fetches on all non-nil attributes, except occasionally the identifier
492
+ # (cf. https://cabig-kc.nci.nih.gov/Bugzilla/show_bug.cgi?id=79).
493
+ # there is no indication of how to identify uniquely searchable attributes, so the secondary key
494
+ # is added manually in the application configuration.
495
+ def finder_parameters(obj)
496
+ key_value_hash(obj, obj.class.primary_key_attributes) or
497
+ key_value_hash(obj, obj.class.secondary_key_attributes) or
498
+ key_value_hash(obj, obj.class.alternate_key_attributes)
499
+ end
500
+
501
+ # Returns the attribute => value hash suitable for a finder template if obj has searchable values
502
+ # for all of the given key attributes, nil otherwise.
503
+ def key_value_hash(obj, attributes)
504
+ # the key must be non-trivial
505
+ return if attributes.nil_or_empty?
506
+ # the attribute => value hash
507
+ attributes.to_compact_hash do |attr|
508
+ value = obj.send(attr)
509
+ # validate that no key attribute is missing and each reference exists
510
+ if value.nil_or_empty? then
511
+ logger.debug { "Can't fetch #{obj.qp} based on #{attributes.qp} since #{attr} does not have a value." }
512
+ return
513
+ elsif obj.class.domain_attribute?(attr) then
514
+ unless exists?(value) then
515
+ logger.debug { "Can't fetch #{obj.qp} based on #{attributes.qp} since #{attr} does not exist in the database: #{value}." }
516
+ return
517
+ end
518
+ # the finder value is a copy of the reference with just the identifier
519
+ value.copy(:identifier)
520
+ else
521
+ value
522
+ end
523
+ end
524
+ end
525
+
526
+ # Returns whether the obj attribute value is either not a domain object reference or exists
527
+ # in the database.
528
+ #
529
+ # Raises DatabaseError if the value is nil.
530
+ def finder_attribute_value_exists?(obj, attr)
531
+ value = obj.send(attr)
532
+ return false if value.nil?
533
+ obj.class.nondomain_attribute?(attr) or value.identifier
534
+ end
535
+
536
+ # Sets the template attribute to a new search reference object created from source.
537
+ # The reference contains only the source identifier.
538
+ # Returns the search reference, or nil if source does not exist in the database.
539
+ def add_search_template_reference(template, source, attribute)
540
+ return if not exists?(source)
541
+ ref = source.copy(:identifier)
542
+ template.set_attribute(attribute, ref)
543
+ # caCORE alert - clear an owner inverse reference, since the template attr assignment might have added a reference
544
+ # from ref to template, which introduces a template => ref => template cycle that causes a caCORE search infinite loop.
545
+ inverse = template.class.attribute_metadata(attribute).derived_inverse
546
+ ref.clear_attribute(inverse) if inverse
547
+ logger.debug { "Search reference parameter #{attribute} for #{template.qp} set to #{ref} copied from #{source.qp}" }
548
+ ref
549
+ end
550
+
551
+ # Takes a {Persistable#snapshot} of obj to track changes and adds a lazy loader.
552
+ # If obj already has a snapshot, then this method is a no-op.
553
+ # If the other fetched source object is given, then the obj snapshot is updated
554
+ # with values from other which were not previously in obj.
555
+ #
556
+ # @param [Resource] obj the domain object to make persistable
557
+ # @param [Resource] other the domain object with the snapshot content
558
+ # @return [Resource] obj
559
+ # @raise [ArgumentError] if obj is a collection and other is not nil
560
+ def persistify(obj, other=nil)
561
+ if obj.collection? then
562
+ if other then raise ArgumentError.new("Database reader persistify other argument not supported") end
563
+ obj.each { |ref| persistify(ref) }
564
+ return obj
565
+ end
566
+ # merge or take a snapshot if necessary
567
+ snapshot(obj, other) unless obj.snapshot_taken?
568
+ # recurse to dependents before adding lazy loader to owner
569
+ obj.dependents.each { |dep| persistify(dep) if dep.identifier }
570
+ # add lazy loader to the unfetched attributes
571
+ add_lazy_loader(obj)
572
+ obj
573
+ end
574
+
575
+ # Adds this database's lazy loader to the given fetched domain object obj.
576
+ def add_lazy_loader(obj)
577
+ obj.add_lazy_loader(@lazy_loader, &@matcher)
578
+ end
579
+
580
+ # If obj has a snapshot and other is given, then merge any new fetched attribute values into the obj snapshot
581
+ # which does not yet have a value for the fetched attribute.
582
+ # Otherwise, take an obj snapshot.
583
+ #
584
+ # @param [Resource] obj the domain object to snapshot
585
+ # @param [Resource] the source domain object
586
+ # @return [Resource] the obj snapshot, updated with source content if necessary
587
+ def snapshot(obj, other=nil)
588
+ if obj.snapshot_taken? then
589
+ if other then
590
+ ovh = other.value_hash(other.class.fetched_attributes)
591
+ obj.snapshot.merge(ovh) { |v, ov| v.nil? ? ov : v }
592
+ end
593
+ else
594
+ obj.take_snapshot
595
+ end
596
+ end
597
+ end
598
+ end
599
+ end