caruby-core 2.1.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
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