caruby-core 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/doc/website/css/site.css +1 -5
- data/doc/website/images/avatar.png +0 -0
- data/doc/website/images/favicon.ico +0 -0
- data/doc/website/images/logo.png +0 -0
- data/doc/website/index.html +82 -0
- data/doc/website/install.html +87 -0
- data/doc/website/quick_start.html +87 -0
- data/doc/website/tissue.html +85 -0
- data/doc/website/uom.html +10 -0
- data/lib/caruby.rb +3 -0
- data/lib/caruby/active_support/README.txt +2 -0
- data/lib/caruby/active_support/core_ext/string.rb +7 -0
- data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/caruby/active_support/inflections.rb +55 -0
- data/lib/caruby/active_support/inflector.rb +398 -0
- data/lib/caruby/cli/application.rb +36 -0
- data/lib/caruby/cli/command.rb +169 -0
- data/lib/caruby/csv/csv_mapper.rb +157 -0
- data/lib/caruby/csv/csvio.rb +185 -0
- data/lib/caruby/database.rb +252 -0
- data/lib/caruby/database/fetched_matcher.rb +66 -0
- data/lib/caruby/database/persistable.rb +432 -0
- data/lib/caruby/database/persistence_service.rb +162 -0
- data/lib/caruby/database/reader.rb +599 -0
- data/lib/caruby/database/saved_merger.rb +131 -0
- data/lib/caruby/database/search_template_builder.rb +59 -0
- data/lib/caruby/database/sql_executor.rb +75 -0
- data/lib/caruby/database/store_template_builder.rb +200 -0
- data/lib/caruby/database/writer.rb +469 -0
- data/lib/caruby/domain/annotatable.rb +25 -0
- data/lib/caruby/domain/annotation.rb +23 -0
- data/lib/caruby/domain/attribute_metadata.rb +447 -0
- data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
- data/lib/caruby/domain/merge.rb +91 -0
- data/lib/caruby/domain/properties.rb +95 -0
- data/lib/caruby/domain/reference_visitor.rb +289 -0
- data/lib/caruby/domain/resource_attributes.rb +528 -0
- data/lib/caruby/domain/resource_dependency.rb +205 -0
- data/lib/caruby/domain/resource_introspection.rb +159 -0
- data/lib/caruby/domain/resource_metadata.rb +117 -0
- data/lib/caruby/domain/resource_module.rb +285 -0
- data/lib/caruby/domain/uniquify.rb +38 -0
- data/lib/caruby/import/annotatable_class.rb +28 -0
- data/lib/caruby/import/annotation_class.rb +27 -0
- data/lib/caruby/import/annotation_module.rb +67 -0
- data/lib/caruby/import/java.rb +338 -0
- data/lib/caruby/migration/migratable.rb +167 -0
- data/lib/caruby/migration/migrator.rb +533 -0
- data/lib/caruby/migration/resource.rb +8 -0
- data/lib/caruby/migration/resource_module.rb +11 -0
- data/lib/caruby/migration/uniquify.rb +20 -0
- data/lib/caruby/resource.rb +969 -0
- data/lib/caruby/util/attribute_path.rb +46 -0
- data/lib/caruby/util/cache.rb +53 -0
- data/lib/caruby/util/class.rb +99 -0
- data/lib/caruby/util/collection.rb +1053 -0
- data/lib/caruby/util/controlled_value.rb +35 -0
- data/lib/caruby/util/coordinate.rb +75 -0
- data/lib/caruby/util/domain_extent.rb +49 -0
- data/lib/caruby/util/file_separator.rb +65 -0
- data/lib/caruby/util/inflector.rb +20 -0
- data/lib/caruby/util/log.rb +95 -0
- data/lib/caruby/util/math.rb +12 -0
- data/lib/caruby/util/merge.rb +59 -0
- data/lib/caruby/util/module.rb +34 -0
- data/lib/caruby/util/options.rb +92 -0
- data/lib/caruby/util/partial_order.rb +36 -0
- data/lib/caruby/util/person.rb +119 -0
- data/lib/caruby/util/pretty_print.rb +184 -0
- data/lib/caruby/util/properties.rb +112 -0
- data/lib/caruby/util/stopwatch.rb +66 -0
- data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
- data/lib/caruby/util/transitive_closure.rb +45 -0
- data/lib/caruby/util/tree.rb +48 -0
- data/lib/caruby/util/trie.rb +37 -0
- data/lib/caruby/util/uniquifier.rb +30 -0
- data/lib/caruby/util/validation.rb +48 -0
- data/lib/caruby/util/version.rb +56 -0
- data/lib/caruby/util/visitor.rb +351 -0
- data/lib/caruby/util/weak_hash.rb +36 -0
- data/lib/caruby/version.rb +3 -0
- 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
|