caruby-core 2.1.1 → 2.1.2

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/Gemfile CHANGED
@@ -2,6 +2,7 @@ source :rubygems
2
2
  gemspec
3
3
 
4
4
  group :development do
5
+ # Uncomment to bind local gems in a local branch. Do not check in.
5
6
  #gem 'jinx', :path => File.dirname(__FILE__) + '/../../jinx/core'
6
7
  #gem 'jinx-json', :path => File.dirname(__FILE__) + '/../../jinx/json'
7
8
  #gem 'jinx-migrate', :path => File.dirname(__FILE__) + '/../../jinx/migrate'
data/History.md CHANGED
@@ -1,7 +1,11 @@
1
1
  This history lists major release themes. See the GitHub Commits (https://github.com/caruby/core)
2
2
  for change details.
3
3
 
4
- 2.1.1 / 2011-04-13
4
+ 2.1.2 / 2012-06-12
5
+ ------------------
6
+ * Support caSmall.
7
+
8
+ 2.1.1 / 2012-04-13
5
9
  ------------------
6
10
  * Simpler, more flexible meta-data loading.
7
11
 
@@ -1,21 +1,32 @@
1
1
  require 'jinx/helpers/lazy_hash'
2
- require 'jinx/helpers/key_transformer_hash'
2
+ require 'jinx/helpers/associative'
3
3
 
4
4
  module CaRuby
5
5
  # Cache for objects held in memory and accessed by key.
6
6
  class Cache
7
- # The classes which are not cleared when {#clear} is called without the +all+ flag.
7
+ # The classes which are not cleared when {#clear} is called.
8
8
  attr_reader :sticky
9
9
 
10
- # @yield [item] returns the key for the given item to cache
11
- # @yieldparam item the object to cache
10
+ # @yield [obj] returns the key for the given object to cache
11
+ # @yieldparam obj the object to cache
12
12
  def initialize
13
- # Make the class => {key => item} hash.
14
- # The {key => item} hash takes an item as an argument and converts
15
- # it to the key by calling the block given to this initializer.
13
+ # Make the class => {object => {key => object}} hash.
14
+ # The {object => {key => object}} hash is an Associative which converts the given
15
+ # object to its key by calling the block given to this initializer.
16
+ # The {{key => object} hash takes a key as an argument and returns the cached object.
17
+ # If there is no cached object, then the object passed to the Associative is cached.
16
18
  @ckh = Jinx::LazyHash.new do
17
- Jinx::KeyTransformerHash.new do |obj|
18
- yield(obj) or Jinx.fail(ArgumentError, "The object to cache does not have a key: #{obj}")
19
+ kh = Hash.new
20
+ # the obj => key associator
21
+ assoc = Jinx::Associative.new do |obj|
22
+ key = yield(obj)
23
+ kh[key] if key
24
+ end
25
+ # the obj => key => value writer
26
+ assoc.writer do |obj, value|
27
+ key = yield(obj)
28
+ raise ArgumentError.new("caRuby cannot cache object without a key: #{obj}") if key.nil?
29
+ kh[key] = value
19
30
  end
20
31
  end
21
32
  @sticky = Set.new
@@ -23,7 +23,7 @@ module CaRuby
23
23
  # @return the attribute value loaded from the database
24
24
  # @raise [RuntimeError] if this loader is disabled
25
25
  def load(subject, attribute)
26
- if disabled? then Jinx.fail(RuntimeError, "#{subject.qp} lazy load called on disabled loader") end
26
+ if disabled? then raise RuntimeError.new("#{subject.qp} lazy load called on disabled loader") end
27
27
  logger.debug { "Lazy-loading #{subject.qp} #{attribute}..." }
28
28
  # the current value
29
29
  oldval = subject.send(attribute)
@@ -29,6 +29,8 @@ module CaRuby
29
29
  def to_s
30
30
  "#{@subject.qp} #{attribute} #{type}"
31
31
  end
32
+
33
+ alias :inspect :to_s
32
34
  end
33
35
  end
34
36
  end
@@ -34,7 +34,7 @@ module CaRuby
34
34
  # @return [Database] the data access mediator for this Persistable, if any
35
35
  # @raise [DatabaseError] if the subclass does not override this method
36
36
  def database
37
- Jinx.fail(ValidationError, "#{self} database is missing")
37
+ raise ValidationError.new("#{self} database is missing")
38
38
  end
39
39
 
40
40
  # @return [PersistenceService] the database application service for this Persistable
@@ -113,6 +113,12 @@ module CaRuby
113
113
  def delete
114
114
  database.delete(self)
115
115
  end
116
+
117
+ # @return [Boolean] whether this domain object can be updated
118
+ # (default is true, subclasses can override)
119
+ def updatable?
120
+ true
121
+ end
116
122
 
117
123
  alias :== :equal?
118
124
 
@@ -138,7 +144,7 @@ module CaRuby
138
144
  # @raise [ValidationError] if this domain object does not have a snapshot
139
145
  def merge_into_snapshot(other)
140
146
  if @snapshot.nil? then
141
- Jinx.fail(ValidationError, "Cannot merge #{other.qp} content into #{qp} snapshot, since #{qp} does not have a snapshot.")
147
+ raise ValidationError.new("Cannot merge #{other.qp} content into #{qp} snapshot, since #{qp} does not have a snapshot.")
142
148
  end
143
149
  # the non-domain attribute => [target value, other value] difference hash
144
150
  delta = diff(other)
@@ -164,7 +170,7 @@ module CaRuby
164
170
 
165
171
  # @return [<Symbol>] the attributes which differ between the {#snapshot} and current content
166
172
  def changed_attributes
167
- if @snapshot then
173
+ if @snapshot then
168
174
  ovh = value_hash(self.class.updatable_attributes)
169
175
  diff = @snapshot.diff(ovh) { |pa, v, ov| Jinx::Resource.value_equal?(v, ov) }
170
176
  diff.keys
@@ -190,7 +196,7 @@ module CaRuby
190
196
  # @param loader [LazyLoader] the lazy loader to add
191
197
  def add_lazy_loader(loader, attributes=nil)
192
198
  # guard against invalid call
193
- if identifier.nil? then Jinx.fail(ValidationError, "Cannot add lazy loader to an unfetched domain object: #{self}") end
199
+ if identifier.nil? then raise ValidationError.new("Cannot add lazy loader to an unfetched domain object: #{self}") end
194
200
  # the attributes to lazy-load
195
201
  attributes ||= loadable_attributes
196
202
  return if attributes.empty?
@@ -252,16 +258,16 @@ module CaRuby
252
258
 
253
259
  # Validates this domain object and its #{Propertied#unproxied_savable_template_attributes}
254
260
  # for consistency and completeness prior to a database create operation.
255
- # An object without an identifer is valid if it contains a non-nil value for each mandatory attribute.
256
- # Objects which have an identifier or have already been validated are skipped.
261
+ # An object is valid if it contains a non-nil value for each mandatory attribute.
262
+ # Objects which have already been validated are skipped.
257
263
  #
258
- # A Persistable class should not override this method, but override the private {#validate_local}
259
- # method instead.
264
+ # A Persistable class should not override this method, but override the
265
+ # private {#validate_local} method instead.
260
266
  #
261
267
  # @return [Persistable] this domain object
262
268
  # @raise [Jinx::ValidationError] if the object state is invalid
263
- def validate
264
- if identifier.nil? and not @validated then
269
+ def validate(autogenerated=false)
270
+ if (identifier.nil? or autogenerated) and not @validated then
265
271
  validate_local
266
272
  @validated = true
267
273
  end
@@ -386,7 +392,6 @@ module CaRuby
386
392
  def copy_volatile_attributes(other)
387
393
  pas = self.class.volatile_nondomain_attributes
388
394
  return if pas.empty?
389
- logger.debug { "Merging volatile attributes #{pas.to_series} from #{other.qp} into #{qp}..." }
390
395
  pas.each do |pa|
391
396
  val = send(pa)
392
397
  oval = other.send(pa)
@@ -396,9 +401,10 @@ module CaRuby
396
401
  logger.debug { "Set #{qp} volatile #{pa} to the fetched #{other.qp} database value #{oval.qp}." }
397
402
  elsif oval != val and pa == :identifier then
398
403
  # If this error occurs, then there is a serious match-merge flaw.
399
- Jinx.fail(DatabaseError, "Can't copy #{other} to #{self} with different identifier")
404
+ raise DatabaseError.new("Can't copy #{other} to #{self} with different identifier")
400
405
  end
401
406
  end
407
+ logger.debug { "Merged auto-generated attribute values #{pas.to_series} from #{other.qp} into #{self}..." }
402
408
  end
403
409
 
404
410
  private
@@ -515,7 +521,7 @@ module CaRuby
515
521
  end
516
522
  end
517
523
  end
518
-
524
+
519
525
  merged
520
526
  end
521
527
 
@@ -19,11 +19,10 @@ module CaRuby
19
19
  # @option opts [String] :host the service host (default +localhost+)
20
20
  # @option opts [String] :version the caTissue version identifier
21
21
  def initialize(name, opts={})
22
- CaRuby::PersistenceService.import_java_classes
23
22
  @name = name
24
23
  ver_opt = opts[:version]
25
24
  @version = ver_opt.to_s.to_version if ver_opt
26
- @host = opts[:host] || default_host
25
+ @host = opts[:host] || 'localhost'
27
26
  @port = opts[:port] || 8080
28
27
  @url = "http://#{@host}:#{@port}/#{@name}/http/remoteService"
29
28
  @timer = Jinx::Stopwatch.new
@@ -52,34 +51,19 @@ module CaRuby
52
51
  # create method. Calling reference attributes of this result is broken by +caCORE+ design.
53
52
  def create(obj)
54
53
  logger.debug { "Submitting create #{obj.pp_s(:single_line)} to application service #{name}..." }
55
- begin
56
- dispatch { |svc| svc.create_object(obj) }
57
- rescue Exception => e
58
- logger.error("Error creating #{obj} - #{e.message}\n#{dump(obj)}")
59
- raise e
60
- end
54
+ dispatch { |svc| svc.create_object(obj) }
61
55
  end
62
56
 
63
57
  # Submits the update to the application service and returns obj.
64
58
  def update(obj)
65
59
  logger.debug { "Submitting update #{obj.pp_s(:single_line)} to application service #{name}..." }
66
- begin
67
- dispatch { |svc| svc.update_object(obj) }
68
- rescue Exception => e
69
- logger.error("Error updating #{obj} - #{e.message}\n#{dump(obj)}")
70
- raise e
71
- end
60
+ dispatch { |svc| svc.update_object(obj) }
72
61
  end
73
62
 
74
63
  # Submits the delete to the application service.
75
64
  def delete(obj)
76
65
  logger.debug { 'Deleting #{obj}.' }
77
- begin
78
- dispatch { |svc| svc.remove_object(obj) }
79
- rescue Exception => e
80
- logger.error("Error deleting #{obj} - #{e.message}\n#{dump(obj)}")
81
- raise e
82
- end
66
+ dispatch { |svc| svc.remove_object(obj) }
83
67
  end
84
68
 
85
69
  # Returns the {ApplicationService} remote instance.
@@ -113,16 +97,6 @@ module CaRuby
113
97
  time { yield app_service }
114
98
  end
115
99
 
116
- # @return [String] the default host name
117
- def default_host
118
- # # TODO - extract from the service config file
119
- # xml = JRuby.runtime.jruby_class_loader.getResourceAsStream('remoteService.xml')
120
- # if xml then
121
- # # parse xml file
122
- # end
123
- 'localhost'
124
- end
125
-
126
100
  # Dispatches the given HQL to the application service.
127
101
  #
128
102
  # @quirk caCORE query target parameter is necessary for caCORE 3.x but deprecated in caCORE 4+.
@@ -132,7 +106,7 @@ module CaRuby
132
106
  logger.debug { "Building HQLCriteria..." }
133
107
  criteria = HQLCriteria.new(hql)
134
108
  target = hql[/from\s+(\S+)/i, 1]
135
- Jinx.fail(DatabaseError, "HQL does not contain a FROM clause: #{hql}") unless target
109
+ raise DatabaseError.new("HQL does not contain a FROM clause: #{hql}") unless target
136
110
  logger.debug { "Submitting search on target class #{target} with the following HQL:\n #{hql}" }
137
111
  begin
138
112
  dispatch { |svc| svc.query(criteria, target) }
@@ -151,7 +125,7 @@ module CaRuby
151
125
  class_name_path = []
152
126
  path.inject(template.class) do |type, pa|
153
127
  ref_type = type.domain_type(pa)
154
- Jinx.fail(DatabaseError, "Property in search attribute path #{path.join('.')} is not a #{type} domain reference attribute: #{pa}") if ref_type.nil?
128
+ raise DatabaseError.new("Property in search attribute path #{path.join('.')} is not a #{type} domain reference attribute: #{pa}") if ref_type.nil?
155
129
  class_name_path << ref_type.java_class.name
156
130
  ref_type
157
131
  end
@@ -180,7 +154,7 @@ module CaRuby
180
154
  begin
181
155
  result = dispatch { |svc| svc.association(obj, assn) }
182
156
  rescue Exception => e
183
- logger.error("Error fetching association #{obj} - #{e.message}\n#{dump(obj)}")
157
+ logger.error("Error fetching association #{obj} - #{e}")
184
158
  raise
185
159
  end
186
160
  end
@@ -191,11 +165,13 @@ module CaRuby
191
165
 
192
166
  private
193
167
 
194
- # Imports this class's Java classes on demand.
195
- def self.import_java_classes
196
- return if const_defined?(:HQLCriteria)
197
- # HQLCriteria is required for the query_hql method.
198
- java_import Java::gov.nih.nci.common.util.HQLCriteria
168
+ # Imports the caCORE +HQLCriteria+ Java class on demand.
169
+ def self.const_missing(sym)
170
+ if sym == :HQLCriteria then
171
+ java_import Java::gov.nih.nci.common.util.HQLCriteria
172
+ else
173
+ super
174
+ end
199
175
  end
200
176
 
201
177
  end
@@ -13,7 +13,18 @@ module CaRuby
13
13
  # Adds query capability to this Database.
14
14
  def initialize
15
15
  super
16
- @ftchd_vstr = Jinx::ReferenceVisitor.new { |ref| ref.class.fetched_domain_attributes }
16
+ # the fetched object cache
17
+ @cache = create_cache
18
+ # the general fetched visitor
19
+ @ftchd_vstr = Jinx::ReferenceVisitor.new() { |ref| ref.class.fetched_domain_attributes }
20
+ # The persistifier visitor recurses to references before adding a lazy loader to the parent.
21
+ # The fetched filter foregoes the visit to a previously fetched reference. The visitor
22
+ # replaces fetched objects with matching cached objects where possible. It is unnecessary
23
+ # to visit a previously persistified cached object.
24
+ pst_flt = Proc.new { |ref| @cache[ref].nil? and not ref.fetched? }
25
+ @pst_vstr = Jinx::ReferenceVisitor.new(:filter => pst_flt, :depth_first => true) do |ref|
26
+ ref.class.fetched_domain_attributes
27
+ end
17
28
  # the demand loader
18
29
  @lazy_loader = LazyLoader.new { |obj, pa| lazy_load(obj, pa) }
19
30
  end
@@ -73,8 +84,14 @@ module CaRuby
73
84
  end
74
85
 
75
86
  # This method clears the given toxic domain objects fetched from the database.
76
- # The copy nondomain attribute values are set to the fetched object values.
77
- # The copy fetched reference attribute values are set to a copy of the result references.
87
+ #
88
+ # Detoxification consists of the following:
89
+ # * The fetched object's class might not have been previously referenced. In that
90
+ # case, introspect the fetched object's class.
91
+ # * Clear toxic references that will result in a caCORE missing session error
92
+ # due to the caCORE API deficiency described below.
93
+ # * Replaced fetched objects with the corresponding cached objects where possible.
94
+ # * Set inverses to enforce inverse integrity where necessary.
78
95
  #
79
96
  # @quirk caCORE Dereferencing a caCORE search result uncascaded collection attribute
80
97
  # raises a Hibernate missing session error.
@@ -88,27 +105,63 @@ module CaRuby
88
105
  # This situation is rectified in this detoxify method by setting the dependent owner
89
106
  # attribute to the fetched owner in the detoxification {Jinx::ReferenceVisitor} copy-match-merge.
90
107
  #
91
- # @return [Jinx::Resource, <Jinx::Resource>] the detoxified object(s)
108
+ # @param [Resource] toxic the fetched object to detoxify
109
+ # @return [Resource, <Resource>] the detoxified object(s)
92
110
  def detoxify(toxic)
93
111
  return if toxic.nil?
94
112
  if toxic.collection? then
95
113
  toxic.each { |obj| detoxify(obj) }
96
114
  else
97
115
  logger.debug { "Detoxifying the toxic caCORE result #{toxic.qp}..." }
98
- @ftchd_vstr.visit(toxic) { |ref| clear_toxic_attributes(ref) }
116
+ @ftchd_vstr.visit(toxic) { |ref| detoxify_object(ref) }
99
117
  logger.debug { "Detoxified the toxic caCORE result #{toxic.qp}." }
100
118
  end
101
119
  toxic
102
120
  end
103
121
 
122
+ # Detoxifies the given domain object. This method is called by {#detoxify} to detoxify a visited
123
+ # object fetched from the database. This method does not detoxify referenced objects.
124
+ #
125
+ # @param (see #detoxify)
126
+ def detoxify_object(toxic)
127
+ ensure_introspected(toxic.class)
128
+ reconcile_fetched_attributes(toxic)
129
+ clear_toxic_attributes(toxic)
130
+ end
131
+
132
+ # Replaces fetched references with cached references where possible.
133
+ #
134
+ # @param (see #detoxify)
135
+ def reconcile_fetched_attributes(toxic)
136
+ toxic.class.fetched_domain_attributes.each_pair do |fa, fp|
137
+ # the fetched reference
138
+ ref = toxic.send(fa) || next
139
+ # the inverse attribute
140
+ fi = fp.inverse
141
+ # Replace each fetched reference with the cached equivalent, if it exists.
142
+ if fp.collection? then
143
+ ref.to_compact_hash { |mbr| @cache[mbr] }.each do |fmbr, cmbr|
144
+ if fmbr != cmbr and (fi.nil? or cmbr.send(fi).nil?) then
145
+ ref.delete(fmbr)
146
+ ref << cmbr
147
+ logger.debug { "Replaced fetched #{toxic} #{fa} #{fmbr} with cached #{cmbr}." }
148
+ end
149
+ end
150
+ else
151
+ cref = @cache[ref]
152
+ if cref and cref != ref and (fi.nil? or cref.send(fi).nil?) then
153
+ toxic.set_property_value(fa, cref)
154
+ logger.debug { "Replaced fetched #{toxic} #{fa} #{ref} with cached #{cref}." }
155
+ end
156
+ end
157
+ end
158
+ end
159
+
104
160
  # Sets each of the toxic attributes in the given domain object to the corresponding
105
161
  # {Metadata#empty_value}.
106
- #
107
- # @param [Jinx::Resource] toxic the toxic domain object
162
+ #
163
+ # @param (see #detoxify)
108
164
  def clear_toxic_attributes(toxic)
109
- # The result class might not have been previously referenced. In that case,
110
- # introspect the class.
111
- ensure_introspected(toxic.class)
112
165
  pas = toxic.class.toxic_attributes
113
166
  return if pas.empty?
114
167
  logger.debug { "Clearing toxic #{toxic.qp} attributes #{pas.to_series}..." }
@@ -117,12 +170,11 @@ module CaRuby
117
170
  next unless prop.java_property?
118
171
  # the empty or nil value to set
119
172
  value = toxic.class.empty_value(pa)
120
- # Use the Java writer method rather than the standard attribute writer method.
121
- # The standard attribute writer enforces inverse integrity, which potential requires
122
- # accessing the current toxic value. The Java writer bypasses inverse integrity.
123
- reader, writer = prop.property_accessors
124
- # clear the attribute
125
- toxic.send(writer, value)
173
+ # Clear the attribute. Use the Java writer method rather than the standard
174
+ # attribute writer method. The standard attribute writer enforces inverse integrity,
175
+ # which potential requires accessing the current toxic value. The Java writer
176
+ # bypasses inverse integrity.
177
+ toxic.send(prop.java_writer, value)
126
178
  end
127
179
  end
128
180
 
@@ -131,21 +183,25 @@ module CaRuby
131
183
  # * Set the inverses to enforce inverse integrity.
132
184
  #
133
185
  # @param (see #persistify_object)
186
+ # @param [Jinx::Resource] other the source domain object
134
187
  # @raise [ArgumentError] if obj is a collection and other is not nil
135
188
  def persistify(obj, other=nil)
136
189
  if obj.collection? then
137
- if other then Jinx.fail(ArgumentError, "Database reader persistify other argument not supported") end
190
+ # A source object is not meaningful for a collection.
191
+ if other then
192
+ raise DatabaseError.new("Database reader persistify other argument is not supported for collection #{obj.qp}")
193
+ end
138
194
  obj.each { |ref| persistify(ref) }
139
195
  return obj
140
196
  end
141
- # The attribute type is introspected, but the object might be an unintrospected
142
- # subtype. Introspect the object class if necessary.
143
- ensure_introspected(obj.class)
144
- # set the inverses before recursing to dependents
197
+ # set the inverses before recursing to references
145
198
  set_inverses(obj)
146
- # recurse to dependents before adding a lazy loader to the owner
147
- obj.dependents.each { |dep| persistify(dep) if dep.identifier }
148
- persistify_object(obj, other)
199
+ @pst_vstr.visit(obj) do |ref|
200
+ persistify_object(ref) if ref.identifier and ref.snapshot.nil?
201
+ end
202
+ # merge the other object content if available
203
+ obj.merge_into_snapshot(other) if other
204
+ obj
149
205
  end
150
206
 
151
207
  # Introspects the given class, if necessary. The class must be either introspected
@@ -166,19 +222,22 @@ module CaRuby
166
222
  # Takes a {Persistable#snapshot} of obj to track changes, adds a lazy loader and
167
223
  # adds the object to the cache.
168
224
  #
169
- # If the other fetched source object is given, then the obj snapshot is updated
225
+ # If the other fetched source object is given, then the snapshot is updated
170
226
  # with the non-nil values from other.
171
227
  #
172
228
  # @param [Jinx::Resource] obj the domain object to make persistable
173
229
  # @param [Jinx::Resource] other the source domain object
174
230
  # @return [Jinx::Resource] obj
175
231
  def persistify_object(obj, other=nil)
232
+ # The attribute type is introspected, but the object might be an unintrospected
233
+ # subtype. Introspect the object class if necessary.
234
+ ensure_introspected(obj.class)
176
235
  # take a snapshot of the database content
177
236
  snapshot(obj, other)
178
237
  # add lazy loader to the unfetched attributes
179
238
  add_lazy_loader(obj)
180
239
  # add to the cache
181
- encache(obj)
240
+ @cache.add(obj)
182
241
  obj
183
242
  end
184
243
 
@@ -214,13 +273,6 @@ module CaRuby
214
273
  # merge the other object content if available
215
274
  obj.merge_into_snapshot(other) if other
216
275
  end
217
-
218
- # @param [Jinx::Resource] obj the object to cache
219
- # @raise [ArgumentError] if the given item does not have an identifier
220
- def encache(obj)
221
- @cache ||= create_cache
222
- @cache.add(obj)
223
- end
224
276
 
225
277
  # @quirk JRuby identifier is not a stable object when fetched from the database, i.e.:
226
278
  # obj.identifier.equal?(obj.identifier) #=> false
@@ -231,10 +283,7 @@ module CaRuby
231
283
  # @return [Cache] a new object cache
232
284
  def create_cache
233
285
  Cache.new do |obj|
234
- if obj.identifier.nil? then
235
- Jinx.fail(ArgumentError, "Can't cache object without identifier: #{obj}")
236
- end
237
- obj.identifier.to_s.to_i
286
+ obj.identifier.to_s.to_i if obj.identifier
238
287
  end
239
288
  end
240
289
  end