caruby-core 1.4.7 → 1.4.9

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.
Files changed (48) hide show
  1. data/History.txt +11 -0
  2. data/README.md +1 -1
  3. data/lib/caruby/cli/command.rb +27 -3
  4. data/lib/caruby/csv/csv_mapper.rb +2 -0
  5. data/lib/caruby/csv/csvio.rb +187 -169
  6. data/lib/caruby/database.rb +33 -16
  7. data/lib/caruby/database/lazy_loader.rb +23 -23
  8. data/lib/caruby/database/persistable.rb +32 -18
  9. data/lib/caruby/database/persistence_service.rb +20 -7
  10. data/lib/caruby/database/reader.rb +22 -21
  11. data/lib/caruby/database/search_template_builder.rb +7 -9
  12. data/lib/caruby/database/sql_executor.rb +52 -27
  13. data/lib/caruby/database/store_template_builder.rb +18 -13
  14. data/lib/caruby/database/writer.rb +107 -44
  15. data/lib/caruby/domain/attribute_metadata.rb +35 -25
  16. data/lib/caruby/domain/java_attribute_metadata.rb +43 -20
  17. data/lib/caruby/domain/merge.rb +9 -5
  18. data/lib/caruby/domain/reference_visitor.rb +4 -3
  19. data/lib/caruby/domain/resource_attributes.rb +52 -12
  20. data/lib/caruby/domain/resource_dependency.rb +129 -42
  21. data/lib/caruby/domain/resource_introspection.rb +1 -1
  22. data/lib/caruby/domain/resource_inverse.rb +20 -3
  23. data/lib/caruby/domain/resource_metadata.rb +20 -4
  24. data/lib/caruby/domain/resource_module.rb +190 -124
  25. data/lib/caruby/import/java.rb +39 -19
  26. data/lib/caruby/migration/migratable.rb +31 -6
  27. data/lib/caruby/migration/migrator.rb +126 -40
  28. data/lib/caruby/migration/uniquify.rb +0 -1
  29. data/lib/caruby/resource.rb +28 -5
  30. data/lib/caruby/util/attribute_path.rb +0 -2
  31. data/lib/caruby/util/class.rb +8 -5
  32. data/lib/caruby/util/collection.rb +5 -3
  33. data/lib/caruby/util/domain_extent.rb +0 -3
  34. data/lib/caruby/util/options.rb +10 -9
  35. data/lib/caruby/util/person.rb +41 -12
  36. data/lib/caruby/util/pretty_print.rb +1 -1
  37. data/lib/caruby/util/validation.rb +0 -28
  38. data/lib/caruby/version.rb +1 -1
  39. data/test/lib/caruby/import/java_test.rb +26 -9
  40. data/test/lib/caruby/migration/test_case.rb +103 -0
  41. data/test/lib/caruby/test_case.rb +231 -0
  42. data/test/lib/caruby/util/class_test.rb +2 -2
  43. data/test/lib/caruby/util/visitor_test.rb +3 -2
  44. data/test/lib/examples/galena/clinical_trials/migration/participant_test.rb +28 -0
  45. data/test/lib/examples/galena/clinical_trials/migration/test_case.rb +40 -0
  46. metadata +195 -170
  47. data/lib/caruby/domain/attribute_initializer.rb +0 -16
  48. data/test/lib/caruby/util/validation_test.rb +0 -14
@@ -11,18 +11,18 @@ module CaRuby
11
11
  # * reader method symbol
12
12
  # * writer method symbol
13
13
  class AttributeMetadata
14
- # The supported attribute qualifier flags.
14
+ # The supported attribute qualifier flags. See the complementary methods for an explanation of
15
+ # the flag option, e.g. {#autogenerated?} for the +:autogenerated+ flag.
15
16
  SUPPORTED_FLAGS = [
16
- :autogenerated, :collection, :dependent, :derived, :logical, :disjoint, :owner, :cascaded,
17
- :no_cascade_update_to_create, :saved, :unsaved, :optional, :fetched, :unfetched,
18
- :create_only, :update_only, :unidirectional, :volatile].to_set
17
+ :autogenerated, :autogenerated_on_update, :collection, :dependent, :derived, :logical, :disjoint,
18
+ :owner, :cascaded, :no_cascade_update_to_create, :saved, :unsaved, :optional, :fetched, :unfetched,
19
+ :include_in_save_template, :saved_fetch, :create_only, :update_only, :unidirectional, :volatile].to_set
19
20
 
20
21
  # @return [(Symbol, Symbol)] the standard attribute reader and writer methods
21
22
  attr_reader :accessors
22
23
 
23
24
  # @return [Class] the declaring class
24
25
  attr_accessor :declarer
25
- protected :declarer=
26
26
 
27
27
  # @return [Class] the return type
28
28
  attr_reader :type
@@ -88,7 +88,7 @@ module CaRuby
88
88
  @type = klass
89
89
  if @inv_md then
90
90
  self.inverse = @inv_md.to_sym
91
- logger.debug { "Reset #{@declarer.qp}.#{self} inverse from #{@inv_md.type}.#{@inv_md} to #{klass}#{inv_md}." }
91
+ logger.debug { "Reset #{@declarer.qp}.#{self} inverse from #{@inv_md.type}.#{@inv_md} to #{klass}#{@inv_md}." }
92
92
  end
93
93
  end
94
94
 
@@ -150,9 +150,8 @@ module CaRuby
150
150
  # if this attribute is disjoint, then so is the inverse.
151
151
  @inv_md.qualify(:disjoint) if disjoint?
152
152
  end
153
-
154
153
  # propagate to restrictions
155
- if @restrictions then @restrictions.each { |attr_md| attr_md.restrict_inverse_type } end
154
+ if @restrictions then @restrictions.each { |attr_md| attr_md.restrict_inverse_type(@inv_md) } end
156
155
  end
157
156
 
158
157
  # @return [AttributeMetadata, nil] the metadata for the {#inverse} attribute, if any
@@ -233,11 +232,21 @@ module CaRuby
233
232
  end
234
233
 
235
234
  # Returns whether the subject attribute is a dependent whose value is automatically generated
236
- # with place-holder domain objects when the parent is created.
235
+ # with place-holder domain objects when the parent is created. An attribute is auto-generated
236
+ # if the +:autogenerate+ or the +:autogenerated_on_update+ flag is set.
237
237
  #
238
238
  # @return [Boolean] whether the attribute is auto-generated
239
239
  def autogenerated?
240
- @flags.include?(:autogenerated)
240
+ @flags.include?(:autogenerated) or @flags.include?(:autogenerated_on_update)
241
+ end
242
+
243
+ # Returns whether the the subject attribute is #{autogenerated?} for create. An attribute is
244
+ # auto-generated for create if the +:autogenerate+ flag is set and the
245
+ # +:autogenerated_on_update+ flag is not set.
246
+ #
247
+ # @return [Boolean] whether the attribute is auto-generated on create
248
+ def autogenerated_on_create?
249
+ @flags.include?(:autogenerated) and not @flags.include?(:autogenerated_on_update)
241
250
  end
242
251
 
243
252
  # Returns whether this attribute must be fetched when a declarer instance is saved.
@@ -248,7 +257,7 @@ module CaRuby
248
257
  # @return [Boolean] whether the subject attribute must be refetched in order to reflect
249
258
  # the database content
250
259
  def saved_fetch?
251
- autogenerated? or (cascaded? and @flags.include?(:unfetched))
260
+ @flags.include?(:saved_fetch) or autogenerated? or (cascaded? and @flags.include?(:unfetched))
252
261
  end
253
262
 
254
263
  # Returns whether the subject attribute is a dependent whose owner does not automatically
@@ -300,23 +309,25 @@ module CaRuby
300
309
  logical? or (dependent? and not creatable?)
301
310
  end
302
311
 
303
- # @return whether this attribute is saved in a update operation
304
- #
305
312
  # A Java attribute is updatable if all of the following conditions hold:
306
313
  # * the attribute is {#saved?}
307
314
  # * the attribute :create_only flag is not set
315
+ #
316
+ # @return [Boolean] whether this attribute is saved in a update operation
308
317
  def updatable?
309
318
  saved? and not @flags.include?(:create_only)
310
319
  end
311
320
 
312
- # @return whether this attribute is navigated to build a template used in a create or update operation
313
- # A cascaded attribute determines where to prune a database create or update object graph.
314
- #
315
- # An attribute is cascaded if it is a physical dependent or the :cascaded flag is set.
321
+ # @return [Boolean] whether the attribute is a physical dependent or the +:cascaded+ flag is set
316
322
  def cascaded?
317
323
  (dependent? and not logical?) or @flags.include?(:cascaded)
318
324
  end
319
325
 
326
+ # @return whether this attribute is {#cascaded?} or marked with the +:include_in_save_template+ flag
327
+ def include_in_save_template?
328
+ cascaded? or @flags.include?(:include_in_save_template)
329
+ end
330
+
320
331
  # Returns whether this attribute is #{#cascaded} and cascades a parent update to a child
321
332
  # create. This corresponds to the Hibernate +save-update+ cascade style but not the Hibernate
322
333
  # +all+ cascade style.
@@ -327,6 +338,8 @@ module CaRuby
327
338
  # Exception: gov.nih.nci.system.applicationservice.ApplicationException:
328
339
  # The given object has a null identifier:
329
340
  # followed by the attribute type name.
341
+ #
342
+ # @return [Boolean] whether the attribute cascades to crate when the owner is updated
330
343
  def cascade_update_to_create?
331
344
  cascaded? and not @flags.include?(:no_cascade_update_to_create)
332
345
  end
@@ -412,12 +425,6 @@ module CaRuby
412
425
  @flags.include?(:disjoint)
413
426
  end
414
427
 
415
- # @param [Symbol] attribute the attribute to check
416
- # @return [Boolean] whether the attribute is part of a 1:1 non-dependency association
417
- def one_to_one_bidirectional_independent?
418
- independent? and not owner? and not collection? and @inv_md and not @inv_md.collection?
419
- end
420
-
421
428
  # @return [Boolean] whether this attribute is a dependent which does not have a Java inverse owner attribute
422
429
  def unidirectional_java_dependent?
423
430
  # TODO - can this be relaxed to java_unidirectional? i.e. eliminate dependent filter
@@ -453,8 +460,11 @@ module CaRuby
453
460
 
454
461
  # If there is a current inverse which can be restricted to an attribute in the scope of
455
462
  # this metadata's restricted type, then reset the inverse to that attribute.
456
- def restrict_inverse_type
457
- return unless @inv_md
463
+ #
464
+ # @param [AttributeMetadata, nil] inv_md the inverse attribute to restrict
465
+ def restrict_inverse_type(inv_md=nil)
466
+ # set the inverse, if necessary
467
+ @inv_md ||= inv_md || return
458
468
  # the current inverse
459
469
  attr = inverse
460
470
  # the restricted type's metadata for the current inverse
@@ -55,6 +55,16 @@ module CaRuby
55
55
  qualify(:collection) if collection_java_class?
56
56
  end
57
57
 
58
+ # @return [Symbol] the JRuby wrapper method for the Java property reader
59
+ def property_reader
60
+ property_accessors.first
61
+ end
62
+
63
+ # @return [Symbol] the JRuby wrapper method for the Java property writer
64
+ def property_writer
65
+ property_accessors.last
66
+ end
67
+
58
68
  def type
59
69
  @type ||= infer_type
60
70
  end
@@ -90,12 +100,15 @@ module CaRuby
90
100
  end
91
101
  end
92
102
 
93
- # Returns whether java_class is an +Iterable+.
103
+ # @return [Boolean] whether this property's Java type is +Iterable+
94
104
  def collection_java_class?
95
- @property_descriptor.property_type.interfaces.any? { |xfc| xfc.java_object == Java::JavaLang::Iterable.java_class }
105
+ # the Java property type
106
+ ptype = @property_descriptor.property_type
107
+ # Test whether the corresponding JRuby wrapper class or module is an Iterable.
108
+ Class.to_ruby(ptype) < Java::JavaLang::Iterable
96
109
  end
97
110
 
98
- # Returns the type for the specified klass property descriptor pd as described in {#initialize}.
111
+ # @return [Class] the type for the specified klass property descriptor pd as described in {#initialize}
99
112
  def infer_type
100
113
  collection_java_class? ? infer_collection_type : infer_non_collection_type
101
114
  end
@@ -103,40 +116,47 @@ module CaRuby
103
116
  # Returns the domain type for this attribute's Java Collection property descriptor.
104
117
  # If the property type is parameterized by a single domain class, then that generic type argument is the domain type.
105
118
  # Otherwise, the type is inferred from the property name as described in {#infer_collection_type_from_name}.
119
+ #
120
+ # @return [Class] this property's Ruby type
106
121
  def infer_collection_type
107
122
  generic_parameter_type or infer_collection_type_from_name or Java::JavaLang::Object
108
123
  end
109
124
 
125
+ # @return [Class] this property's Ruby type
110
126
  def infer_non_collection_type
111
- prop_type = @property_descriptor.property_type
112
- if prop_type.primitive then
113
- Class.to_ruby(prop_type)
127
+ jtype = @property_descriptor.property_type
128
+ if jtype.primitive then
129
+ Class.to_ruby(jtype)
114
130
  else
115
- @declarer.domain_module.domain_type_with_name(prop_type.name) or Class.to_ruby(prop_type)
131
+ @declarer.domain_module.domain_type_with_name(jtype.name) or Class.to_ruby(jtype)
116
132
  end
117
133
  end
118
134
 
135
+ # @return [Class, nil] the Ruby type as determined by the configuration, if any
119
136
  def configured_type
120
137
  name = @declarer.class.configuration.domain_type_name(to_sym) || return
121
138
  @declarer.domain_module.domain_type_with_name(name) or java_to_ruby_class(name)
122
139
  end
123
140
 
124
- # Returns the domain type of this attribute's property descriptor Collection generic type argument, or nil if none.
141
+ # @return [Class, nil] the domain type of this attribute's property descriptor Collection generic
142
+ # type argument, or nil if none
125
143
  def generic_parameter_type
126
144
  method = @property_descriptor.readMethod || return
127
- prop_type = method.genericReturnType
128
- return unless Java::JavaLangReflect::ParameterizedType === prop_type
129
- arg_types = prop_type.actualTypeArguments
130
- return unless arg_types.size == 1
131
- arg_type = arg_types[0]
132
- klass = java_to_ruby_class(arg_type)
133
- logger.debug { "Inferred #{declarer.qp} #{self} domain type #{klass.qp} from generic parameter #{arg_type.name}." } if klass
145
+ gtype = method.genericReturnType
146
+ return unless Java::JavaLangReflect::ParameterizedType === gtype
147
+ atypes = gtype.actualTypeArguments
148
+ return unless atypes.size == 1
149
+ atype = atypes[0]
150
+ klass = java_to_ruby_class(atype)
151
+ logger.debug { "Inferred #{declarer.qp} #{self} domain type #{klass.qp} from generic parameter #{atype.name}." } if klass
134
152
  klass
135
153
  end
136
154
 
137
- def java_to_ruby_class(java_type)
138
- java_type = java_type.name unless String === java_type
139
- @declarer.domain_module.domain_type_with_name(java_type) or Class.to_ruby(java_type)
155
+ # @param [Class, String] jtype the Java class or class name
156
+ # @return [Class] the corresponding Ruby type
157
+ def java_to_ruby_class(jtype)
158
+ name = String === jtype ? jtype : jtype.name
159
+ @declarer.domain_module.domain_type_with_name(name) or Class.to_ruby(name)
140
160
  end
141
161
 
142
162
  # Returns the domain type for this attribute's collection Java property descriptor name.
@@ -148,13 +168,16 @@ module CaRuby
148
168
  # is inferred as +DistributionProtocol+ by stripping the +Collection+ suffix,
149
169
  # capitalizing the prefix and looking for a class of that name in this classifier's
150
170
  # domain_module.
171
+ #
172
+ # @return [Class] the collection item type
151
173
  def infer_collection_type_from_name
152
174
  prop_name = @property_descriptor.name
153
175
  index = prop_name =~ /Collection$/
154
176
  index ||= prop_name.length
155
177
  prefix = prop_name[0...1].upcase + prop_name[1...index]
156
- logger.debug { "Inferring #{declarer.qp} #{self} domain type from attribute name prefix #{prefix}..." }
157
- @declarer.domain_module.domain_type_with_name(prefix)
178
+ klass = @declarer.domain_module.domain_type_with_name(prefix)
179
+ if klass then logger.debug { "Inferred #{declarer.qp} #{self} collection domain type #{klass.qp} from the attribute name." } end
180
+ klass
158
181
  end
159
182
  end
160
183
  end
@@ -1,3 +1,5 @@
1
+ require 'caruby/util/validation'
2
+
1
3
  module CaRuby
2
4
  # A Mergeable supports merging {Resource} attribute values.
3
5
  module Mergeable
@@ -74,7 +76,7 @@ module CaRuby
74
76
  # @return the merged attribute value
75
77
  def merge_attribute(attribute, newval, matches=nil)
76
78
  # the previous value
77
- oldval = send(attribute)
79
+ oldval = send(attribute)
78
80
  # If nothing to merge or a block can take over, then bail.
79
81
  if newval.nil? or mergeable__equal?(oldval, newval) then
80
82
  return oldval
@@ -132,10 +134,12 @@ module CaRuby
132
134
  if matches && matches.has_key?(src) then
133
135
  # the source match
134
136
  tgt = matches[src]
135
- if oldval.include?(tgt) then
136
- tgt.merge_attributes(src)
137
- else
138
- adds << tgt
137
+ if tgt then
138
+ if oldval.include?(tgt) then
139
+ tgt.merge_attributes(src)
140
+ else
141
+ adds << tgt
142
+ end
139
143
  end
140
144
  else
141
145
  adds << src
@@ -2,6 +2,7 @@ require 'enumerator'
2
2
  require 'generator'
3
3
  require 'caruby/util/options'
4
4
  require 'caruby/util/collection'
5
+ require 'caruby/util/validation'
5
6
  require 'caruby/util/visitor'
6
7
  require 'caruby/util/math'
7
8
 
@@ -184,13 +185,13 @@ module CaRuby
184
185
  # @yieldparam [Resource] source the visited source domain object
185
186
  # @yieldparam [Resource] target the domain object which matches the visited source
186
187
  def visit_matched(source)
187
- target = match_for_visited(source)
188
+ tgt = match_for_visited(source)
188
189
  # match the matchable references, if any
189
190
  if @matchable then
190
191
  attrs = @matchable.call(source) - attributes_to_visit(source)
191
- attrs.each { |attr| match_reference(source, target, attr) }
192
+ attrs.each { |attr| match_reference(source, tgt, attr) }
192
193
  end
193
- block_given? ? yield(source, target) : target
194
+ block_given? ? yield(source, tgt) : tgt
194
195
  end
195
196
 
196
197
  # @param source (see #match_visited)
@@ -129,6 +129,8 @@ module CaRuby
129
129
  @log_dep_attrs ||= dependent_attributes.compose { |attr_md| attr_md.logical? }
130
130
  end
131
131
 
132
+ # @return [<Symbol>] the unidirectional dependent attributes
133
+ # @see AttributeMetadata#unidirectional?
132
134
  def unidirectional_dependent_attributes
133
135
  @uni_dep_attrs ||= dependent_attributes.compose { |attr_md| attr_md.unidirectional? }
134
136
  end
@@ -165,14 +167,19 @@ module CaRuby
165
167
 
166
168
  # @return [<Symbol>] the {#cascaded_attributes} which are saved with a proxy
167
169
  # using the dependent saver_proxy method
168
- def proxied_cascaded_attributes
169
- @px_cscd_attrs ||= cascaded_attributes.compose { |attr_md| attr_md.proxied_save? }
170
+ def proxied_save_template_attributes
171
+ @px_cscd_attrs ||= save_template_attributes.compose { |attr_md| attr_md.proxied_save? }
170
172
  end
171
173
 
172
174
  # @return [<Symbol>] the {#cascaded_attributes} which do not have a
173
175
  # #{AttributeMetadata#proxied_save?}
174
- def unproxied_cascaded_attributes
175
- @unpx_cscd_attrs ||= cascaded_attributes.compose { |attr_md| not attr_md.proxied_save? }
176
+ def unproxied_save_template_attributes
177
+ @unpx_sv_tmpl_attrs ||= save_template_attributes.compose { |attr_md| not attr_md.proxied_save? }
178
+ end
179
+
180
+ # @return [<Symbol>] the {#domain_attributes} to {AttributeMetadata#include_in_save_template?}
181
+ def save_template_attributes
182
+ @sv_tmpl_attrs ||= domain_attributes.compose { |attr_md| attr_md.include_in_save_template? }
176
183
  end
177
184
 
178
185
  # Returns the physical or auto-generated logical dependent attributes that can
@@ -317,9 +324,9 @@ module CaRuby
317
324
  # @param [{Symbol => AttributeMetadata}] hash the attribute symbol => metadata hash
318
325
  # @yield [attr_md] condition which determines whether the attribute is selected
319
326
  # @yieldparam [AttributeMetadata] the metadata for the standard attribute
320
- def initialize(hash, &filter)
321
- raise ArgumentError.new("Attribute filter missing hash argument") if hash.nil?
322
- raise ArgumentError.new("Attribute filter missing filter block") unless block_given?
327
+ def initialize(klass, hash, &filter)
328
+ raise ArgumentError.new("#{klass.qp} attribute filter missing hash argument") if hash.nil?
329
+ raise ArgumentError.new("#{klass.qp} attribute filter missing filter block") unless block_given?
323
330
  @hash = hash
324
331
  @filter = filter
325
332
  end
@@ -345,6 +352,14 @@ module CaRuby
345
352
  each_pair { |attr, attr_md| yield(attr_md) }
346
353
  end
347
354
 
355
+ # @yield [attribute] the block to apply to the attribute
356
+ # @yieldparam [Symbol] attribute the attribute
357
+ # @return [AttributeMetadata] the first attribute metadata satisfies the block
358
+ def detect_metadata
359
+ each_pair { |attr, attr_md| return attr_md if yield(attr) }
360
+ nil
361
+ end
362
+
348
363
  # @yield [attr_md] the block to apply to the attribute metadata
349
364
  # @yieldparam [AttributeMetadata] attr_md the attribute metadata
350
365
  # @return [Symbol] the first attribute whose metadata satisfies the block
@@ -358,7 +373,7 @@ module CaRuby
358
373
  # @return [Filter] a new Filter which applies the filter block given to this
359
374
  # method with the AttributeMetadata enumerated by this filter
360
375
  def compose
361
- Filter.new(@hash) { |attr_md| @filter.call(attr_md) and yield(attr_md) }
376
+ Filter.new(self, @hash) { |attr_md| @filter.call(attr_md) and yield(attr_md) }
362
377
  end
363
378
  end
364
379
 
@@ -368,7 +383,7 @@ module CaRuby
368
383
  # @yield [attr_md] the attribute selector
369
384
  # @yieldparam [AttributeMetadata] attr_md the candidate attribute
370
385
  def attribute_filter(&filter)
371
- Filter.new(@attr_md_hash, &filter)
386
+ Filter.new(self, @attr_md_hash, &filter)
372
387
  end
373
388
 
374
389
  # Initializes the attribute meta-data structures.
@@ -382,10 +397,23 @@ module CaRuby
382
397
  @local_defaults = {}
383
398
  @defaults = append_ancestor_enum(@local_defaults) { |par| par.defaults }
384
399
  end
400
+
401
+
402
+ # Creates the given attribute alias. Not that unlike {Class#alias_attribute}, this method creates a new
403
+ # alias reader (writer) method which delegates to the attribute reader (writer, resp.) rather than aliasing
404
+ # the existing reader or writer method. This allows the alias to pick up run-time redefinitions of the
405
+ # aliased reader and writer.
406
+ #
407
+ # @param [Symbol] aliaz the attribute alias
408
+ # @param [Symbol] attribute the attribute to alias
409
+ def alias_attribute(aliaz, attribute)
410
+ add_attribute_aliases(aliaz => attribute)
411
+ end
385
412
 
386
413
  # Creates the given aliases to attributes.
387
414
  #
388
415
  # @param [{Symbol => Symbol}] hash the alias => attribute hash
416
+ # @see #attribute_alias
389
417
  def add_attribute_aliases(hash)
390
418
  hash.each { |aliaz, attr| delegate_to_attribute(aliaz, attr) }
391
419
  end
@@ -414,10 +442,11 @@ module CaRuby
414
442
  # Otherwise, if the attribute type is unspecified or is a superclass of the given class,
415
443
  # then make a new attribute metadata for this class.
416
444
  if attr_md.declarer == self then
445
+ logger.debug { "Set #{qp}.#{attribute} type to #{klass.qp}." }
417
446
  attr_md.type = klass
418
447
  elsif attr_md.type.nil? or klass < attr_md.type then
419
- logger.debug { "Restricting #{attr_md.declarer.qp}.#{attribute}(#{attr_md.type.qp}) to #{qp} with return type #{klass.qp}..." }
420
448
  new_attr_md = attr_md.restrict_type(self, klass)
449
+ logger.debug { "Restricted #{attr_md.declarer.qp}.#{attribute}(#{attr_md.type.qp}) to #{qp} with return type #{klass.qp}." }
421
450
  add_attribute_metadata(new_attr_md)
422
451
  elsif klass != attr_md.type then
423
452
  raise ArgumentError.new("Cannot reset #{qp}.#{attribute} type #{attr_md.type} to incompatible #{klass.qp}")
@@ -481,7 +510,10 @@ module CaRuby
481
510
  # @return [Enumerable] the {Enumerable#union} of the base collection with the superclass
482
511
  # collection, if applicable
483
512
  def append_ancestor_enum(enum)
484
- superclass < Resource ? enum.union(yield(superclass)) : enum
513
+ return enum unless superclass < Resource
514
+ anc_enum = yield superclass
515
+ if anc_enum.nil? then raise MetadataError.new("#{qp} superclass #{superclass.qp} does not have required metadata") end
516
+ enum.union(anc_enum)
485
517
  end
486
518
 
487
519
  def each_attribute_metadata(&block)
@@ -508,12 +540,20 @@ module CaRuby
508
540
  # add the secondary key
509
541
  mandatory.merge(secondary_key_attributes)
510
542
  # add the owner attribute, if any
511
- mandatory << owner_attribute unless owner_attribute.nil? or not attribute_metadata(owner_attribute).java_property?
543
+ oattr = mandatory_owner_attribute
544
+ mandatory << oattr if oattr
512
545
  # remove autogenerated or optional attributes
513
546
  mandatory.delete_if { |attr| attribute_metadata(attr).autogenerated? or attribute_metadata(attr).optional? }
514
547
  @local_mndty_attrs.merge!(mandatory)
515
548
  append_ancestor_enum(@local_mndty_attrs) { |par| par.mandatory_attributes }
516
549
  end
550
+
551
+ # @return [Symbol, nil] the unique non-self-referential owner attribute, if one exists
552
+ def mandatory_owner_attribute
553
+ attr = owner_attribute || return
554
+ attr_md = attribute_metadata(attr)
555
+ attr if attr_md.java_property? and attr_md.type != self
556
+ end
517
557
 
518
558
  # Raises a NameError. Domain classes can override this method to dynamically create a new reference attribute.
519
559
  #