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,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