caruby-core 1.4.7 → 1.4.9

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