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