caruby-core 1.4.2 → 1.4.3

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 (52) hide show
  1. data/History.txt +10 -0
  2. data/lib/caruby/cli/command.rb +10 -8
  3. data/lib/caruby/database/fetched_matcher.rb +28 -39
  4. data/lib/caruby/database/lazy_loader.rb +101 -0
  5. data/lib/caruby/database/persistable.rb +190 -167
  6. data/lib/caruby/database/persistence_service.rb +21 -7
  7. data/lib/caruby/database/persistifier.rb +185 -0
  8. data/lib/caruby/database/reader.rb +106 -176
  9. data/lib/caruby/database/saved_matcher.rb +56 -0
  10. data/lib/caruby/database/search_template_builder.rb +1 -1
  11. data/lib/caruby/database/sql_executor.rb +8 -7
  12. data/lib/caruby/database/store_template_builder.rb +134 -61
  13. data/lib/caruby/database/writer.rb +252 -52
  14. data/lib/caruby/database.rb +88 -67
  15. data/lib/caruby/domain/attribute_initializer.rb +16 -0
  16. data/lib/caruby/domain/attribute_metadata.rb +161 -72
  17. data/lib/caruby/domain/id_alias.rb +22 -0
  18. data/lib/caruby/domain/inversible.rb +91 -0
  19. data/lib/caruby/domain/merge.rb +116 -35
  20. data/lib/caruby/domain/properties.rb +1 -1
  21. data/lib/caruby/domain/reference_visitor.rb +207 -71
  22. data/lib/caruby/domain/resource_attributes.rb +93 -80
  23. data/lib/caruby/domain/resource_dependency.rb +22 -97
  24. data/lib/caruby/domain/resource_introspection.rb +21 -28
  25. data/lib/caruby/domain/resource_inverse.rb +134 -0
  26. data/lib/caruby/domain/resource_metadata.rb +41 -19
  27. data/lib/caruby/domain/resource_module.rb +42 -33
  28. data/lib/caruby/import/java.rb +8 -9
  29. data/lib/caruby/migration/migrator.rb +20 -7
  30. data/lib/caruby/migration/resource_module.rb +0 -2
  31. data/lib/caruby/resource.rb +132 -351
  32. data/lib/caruby/util/cache.rb +4 -1
  33. data/lib/caruby/util/class.rb +48 -1
  34. data/lib/caruby/util/collection.rb +54 -18
  35. data/lib/caruby/util/inflector.rb +7 -0
  36. data/lib/caruby/util/options.rb +35 -31
  37. data/lib/caruby/util/partial_order.rb +1 -1
  38. data/lib/caruby/util/properties.rb +2 -2
  39. data/lib/caruby/util/stopwatch.rb +16 -8
  40. data/lib/caruby/util/transitive_closure.rb +1 -1
  41. data/lib/caruby/util/visitor.rb +342 -328
  42. data/lib/caruby/version.rb +1 -1
  43. data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
  44. data/lib/caruby.rb +2 -0
  45. metadata +10 -9
  46. data/lib/caruby/database/saved_merger.rb +0 -131
  47. data/lib/caruby/domain/annotatable.rb +0 -25
  48. data/lib/caruby/domain/annotation.rb +0 -23
  49. data/lib/caruby/import/annotatable_class.rb +0 -28
  50. data/lib/caruby/import/annotation_class.rb +0 -27
  51. data/lib/caruby/import/annotation_module.rb +0 -67
  52. data/lib/caruby/migration/resource.rb +0 -8
@@ -6,6 +6,7 @@ require 'caruby/util/options'
6
6
  require 'caruby/util/visitor'
7
7
  require 'caruby/util/inflector'
8
8
  require 'caruby/database/persistable'
9
+ require 'caruby/database/persistifier'
9
10
  require 'caruby/database/reader'
10
11
  require 'caruby/database/writer'
11
12
  require 'caruby/database/persistence_service'
@@ -42,9 +43,38 @@ module CaRuby
42
43
  # store method. CaRuby::Resource sets reasonable default values, recognizes application dependencies and steers
43
44
  # around caBIG idiosyncracies to the extent possible.
44
45
  class Database
45
- include Reader, Writer, Validation
46
+ include Reader, Writer, Persistifier, Validation
47
+
48
+ # Database CRUD operation.
49
+ class Operation
50
+ attr_reader :type, :subject, :attribute
51
+
52
+ # @param [:find, :query, :create, :udate, :delete] type the database operation type
53
+ # @param [Persistable] subject the domain object on which the operation is performed
54
+ # @param [{Symbol => Object}, Symbol, nil] the operation characteristics
55
+ # @option opts [Symbol] :attribute the query attribute
56
+ # @option opts [Boolean] :autogenerated whether this is an auto-generated subject update
57
+ def initialize(type, subject, opts=nil)
58
+ @type = type
59
+ @subject = subject
60
+ @attribute = Options.get(:attribute, opts)
61
+ @autogenerated = Options.get(:autogenerated, opts, false)
62
+ end
63
+
64
+ # @return [Boolean] whether this operation is an update of an auto-generated subject
65
+ def autogenerated?
66
+ @autogenerated
67
+ end
68
+
69
+ def to_s
70
+ "#{@subject.qp} #{attribute} #{type}"
71
+ end
72
+ end
46
73
 
47
74
  attr_reader :operations
75
+
76
+ # @return [PersistenceService] the services used by this database
77
+ attr_reader :persistence_services
48
78
 
49
79
  # Creates a new Database with the specified service name and options.
50
80
  #
@@ -64,6 +94,7 @@ module CaRuby
64
94
  @host = Options.get(:host, opts)
65
95
  # class => service hash; default is the catissuecore app service
66
96
  @def_persist_svc = PersistenceService.new(service_name, :host => @host)
97
+ @persistence_services = [@def_persist_svc].to_set
67
98
  @cls_svc_hash = Hash.new(@def_persist_svc)
68
99
  # the create/update nested operations
69
100
  @operations = []
@@ -82,6 +113,11 @@ module CaRuby
82
113
  # call the block and close when done
83
114
  yield(self) ensure close
84
115
  end
116
+
117
+ # Clears the cache.
118
+ def clear
119
+ @cache.clear
120
+ end
85
121
 
86
122
  # Releases database resources. This method should be called when database interaction
87
123
  # is completed.
@@ -93,46 +129,49 @@ module CaRuby
93
129
  logger.error("Session termination unsuccessful - #{e.message}")
94
130
  end
95
131
  # clear the cache
96
- @cache.clear
132
+ clear
97
133
  logger.info("Disconnected from application server.")
98
134
  @session = nil
99
135
  end
100
136
 
101
- # Returns the execution time spent since the last open.
102
- def execution_time
137
+ # @return [Numeric] the execution time in seconds spent since the last open
138
+ def execution_time
103
139
  persistence_services.inject(0) do |total, svc|
104
140
  st = svc.timer.elapsed
105
141
  total + st
106
142
  end
107
143
  end
108
144
 
109
- # Returns the PersistanceService to use for the given domain object obj,
110
- # or the default service if obj is nil.
111
- def persistence_service(obj=nil)
145
+ # Returns the PersistanceService to use for the given domain object.
146
+ # This base method always returns the standard application service.
147
+ # Subclasses can override for specialized services. A session is
148
+ # started on demand if necessary.
149
+ #
150
+ # @param [Persistable] obj the domain object
151
+ # @return [PersistanceService] the service for the domain object
152
+ def persistence_service(obj)
112
153
  start_session if @session.nil?
113
- return @def_persist_svc if obj.nil?
114
- klass = Class === obj ? obj : obj.class
115
- @cls_svc_hash[klass]
116
- end
117
-
118
- # Returns all PersistanceServices used by this database.
119
- def persistence_services
120
- [@def_persist_svc].to_set.merge!(@cls_svc_hash.values)
154
+ @def_persist_svc
121
155
  end
122
156
 
123
- # Returns the database operation elapsed real time since the last open.
124
- def database_time
125
- persistence_services.inject(0) do |total, svc|
126
- st = svc.timer.elapsed
127
- # reset the timer for the next test case
128
- svc.timer.reset
129
- total + st
130
- end
157
+ # Adds the given service to this database.
158
+ #
159
+ # @param [PersistenceService] service the service to add
160
+ def add_persistence_service(service)
161
+ @persistence_services << service
131
162
  end
163
+
164
+ alias :to_s :print_class_and_id
165
+
166
+ alias :inspect :to_s
167
+
168
+ ## Utility classes and methods, used by Query and Store mix-ins ##
169
+
170
+ private
132
171
 
133
172
  # A mergeable autogenerated operation is recursively defined as:
134
- # * a create
135
- # * an update in the context of a mergeable autogenerated operation
173
+ # * a create of an object with auto-generated dependents
174
+ # * an update of an auto-generated dependent in the context of a mergeable autogenerated operation
136
175
  #
137
176
  # @return whether the innermost operation conforms to the above criterion
138
177
  def mergeable_autogenerated_operation?
@@ -145,7 +184,7 @@ module CaRuby
145
184
  end
146
185
  if op.type == :create then
147
186
  # innermost or owner create
148
- return true
187
+ return (not op.subject.class.autogenerated_dependent_attributes.empty?)
149
188
  elsif op.type != :update then
150
189
  # not a save
151
190
  return false
@@ -155,65 +194,47 @@ module CaRuby
155
194
  end
156
195
  false
157
196
  end
158
-
159
- alias :to_s :print_class_and_id
160
-
161
- alias :inspect :to_s
162
-
163
- private
164
-
165
- ## Utility classes and methods, used by Query and Store mix-ins ##
166
-
167
- # Database CRUD operation.
168
- class Operation
169
- attr_reader :type, :subject, :attribute
170
-
171
- def initialize(type, subject, attribute=nil)
172
- @type = type
173
- @subject = subject
174
- @attribute = attribute
175
- end
176
- end
177
-
197
+
178
198
  # Performs the operation given by the given op symbol on obj by calling the block given to this method.
179
- # Returns the result of calling the block.
180
- # Valid op symbols are described in {Operation#initialize}.
181
- def perform(op, obj, attribute=nil)
199
+ # Lazy loading is suspended during the operation.
200
+ #
201
+ # @param [:find, :query, :create, :udate, :delete] op the database operation type
202
+ # @param [Resource] obj the domain object on which the operation is performed
203
+ # @param opts (#see Operation#initialize)
204
+ # @yield the database operation block
205
+ # @return the result of calling the operation block
206
+ def perform(op, obj, opts=nil)
182
207
  op_s = op.to_s.capitalize_first
183
- attr_s = " #{attribute}" if attribute
208
+ attr = Options.get(:attribute, opts)
209
+ attr_s = " #{attr}" if attr
210
+ ag_s = " autogenerated" if Options.get(:autogenerated, opts)
184
211
  ctxt_s = " in context #{print_operations}" unless @operations.empty?
185
- logger.info(">> #{op_s} #{obj.pp_s(:single_line)}#{attr_s}#{ctxt_s}...")
186
- @operations.push(Operation.new(op, obj, attribute))
212
+ logger.info(">> #{op_s}#{ag_s} #{obj.pp_s(:single_line)}#{attr_s}#{ctxt_s}...")
213
+ @operations.push(Operation.new(op, obj, opts))
187
214
  begin
188
215
  # perform the operation
189
- result = yield
216
+ result = @lazy_loader.suspend { yield }
190
217
  ensure
191
218
  # the operation is done
192
219
  @operations.pop
193
- # If this is a top-level operation, then clear the cache and transient set.
194
- if @operations.empty? then
195
- @cache.clear
196
- @transients.clear
197
- end
220
+ # If this is a top-level operation, then clear the transient set.
221
+ if @operations.empty? then @transients.clear end
198
222
  end
199
223
  logger.info("<< Completed #{obj.qp}#{attr_s} #{op}.")
200
224
  result
201
225
  end
202
-
226
+
227
+ def each_persistence_service(&block)
228
+ ObjectSpace.each_object(PersistenceService, &block)
229
+ end
230
+
203
231
  # @return [Cache] a new object cache.
204
232
  def create_cache
205
233
  # JRuby alert - identifier is not a stable object when fetched from the database, i.e.:
206
234
  # obj.identifier.equal?(obj.identifier) #=> false
207
235
  # This is probably an artifact of jRuby Numeric - Java Long conversion interaction
208
236
  # combined with hash access use of the eql? method. Work-around is to make a Ruby Integer.
209
- # the fetched object copier
210
- copier = Proc.new do |src|
211
- copy = src.copy
212
- logger.debug { "Fetched #{src.qp} copied to #{copy.qp}." }
213
- copy
214
- end
215
- # the fetched object cache
216
- Cache.new(copier) do |obj|
237
+ Cache.new do |obj|
217
238
  raise ArgumentError.new("Can't cache object without identifier: #{obj}") unless obj.identifier
218
239
  obj.identifier.to_s.to_i
219
240
  end
@@ -0,0 +1,16 @@
1
+ module CaRuby
2
+ module AttributeInitializer
3
+ # Initializes a new instance of this Resource class with optional attribute => value hash
4
+ # @param [{Symbol => Object}] the optional attribute => value hash
5
+ # @return
6
+ def initialize(hash=nil)
7
+ super()
8
+ if hash then
9
+ unless Hashable === hash then
10
+ raise ArgumentError.new("#{qp} initializer argument type not supported: #{hash.class.qp}")
11
+ end
12
+ merge_attributes(hash)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -14,23 +14,22 @@ module CaRuby
14
14
  # The supported attribute qualifier flags.
15
15
  SUPPORTED_FLAGS = [
16
16
  :autogenerated, :collection, :dependent, :derived, :logical, :disjoint, :owner, :cascaded,
17
- :no_cascade_update_to_create, :saved, :unsaved, :saved_unmergeable, :optional, :fetched, :unfetched,
17
+ :no_cascade_update_to_create, :saved, :unsaved, :optional, :fetched, :unfetched,
18
18
  :create_only, :update_only, :unidirectional, :volatile].to_set
19
19
 
20
- # The standard attribute reader and writer methods.
20
+ # @return [(Symbol, Symbol)] the standard attribute reader and writer methods
21
21
  attr_reader :accessors
22
22
 
23
- # The declaring class.
23
+ # @return [Class] the declaring class
24
24
  attr_accessor :declarer
25
+ protected :declarer=
25
26
 
26
- # The return class.
27
- attr_accessor :type
27
+ # @return [Class] the return type
28
+ attr_reader :type
28
29
 
29
- # The qualifier flags.
30
+ # @return [<Symbol>] the qualifier flags
30
31
  # @see SUPPORTED_FLAGS
31
32
  attr_accessor :flags
32
-
33
- protected :declarer=
34
33
 
35
34
  # Creates a new AttributeMetadata from the given attribute.
36
35
  #
@@ -82,33 +81,41 @@ module CaRuby
82
81
  def unidirectional?
83
82
  inverse.nil?
84
83
  end
84
+
85
+ # @param [Class] the attribute return type
86
+ def type=(klass)
87
+ return if klass == @type
88
+ @type = klass
89
+ if @inv_md then
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}." }
92
+ end
93
+ end
85
94
 
86
95
  # Creates a new declarer attribute which restricts this attribute {#type} to the given type.
87
96
  #
88
- #@param [Class] declarer the class for which the restriction holds
89
- # @return [AttributeMetadata] the metadata for the new declarer attribute
90
- def restrict(declarer, type)
91
- unless declarer < self.type then
92
- raise ArgumentError.new("Cannot restrict #{self.declarer}.#{self} to incompatible declarer type #{declarer}")
93
- end
97
+ # @param declarer (see #restrict)
98
+ # @param [Class] type the restricted subclass of this attribute's return type
99
+ # @return (see #restrict)
100
+ def restrict_type(declarer, type)
94
101
  if self.type and not type < self.type then
95
- raise ArgumentError.new("Cannot restrict #{self.declarer}.#{self} to incompatible type #{type}")
102
+ raise ArgumentError.new("Cannot restrict #{self.declarer.qp}.#{self} to incompatible attribute type #{type.qp}")
96
103
  end
97
- # set aside the restrictions prior to the copy
98
- rstr = @restrictions
99
- @restrictions = nil
100
- copy = dup
101
- # Restore the restrictions. Initialize the array if necessary, since the copy
102
- # will be added to the list.
103
- @restrictions = rstr || []
104
- # specialize the copy declarer and type
105
- copy.declarer = declarer
104
+ copy = restrict(declarer)
106
105
  copy.type = type
107
106
  # specialize the inverse to the restricted type attribute, if necessary
108
- if inverse then copy.inverse = inverse end
109
- # Capture the restriction to propagate modifications to this metadata, esp.
110
- # adding an inverse.
111
- @restrictions << copy
107
+ copy.restrict_inverse_type
108
+ copy
109
+ end
110
+
111
+ # Creates a new declarer attribute which qualifies this attribute for the given declarer.
112
+ #
113
+ # @param declarer (see #restrict)
114
+ # @param [<Symbol>] flags the additional flags for the restricted attribute
115
+ # @return (see #restrict)
116
+ def restrict_flags(declarer, *flags)
117
+ copy = restrict(declarer)
118
+ copy.qualify(*flags)
112
119
  copy
113
120
  end
114
121
 
@@ -118,26 +125,34 @@ module CaRuby
118
125
  #
119
126
  # @param attribute the inverse attribute
120
127
  def inverse=(attribute)
128
+ return if inverse == attribute
129
+ # if no attribute, then the clear the existing inverse, if any
130
+ return clear_inverse if attribute.nil?
131
+
121
132
  logger.debug { "Set #{@declarer.qp}.#{self} inverse to #{type.qp}.#{attribute}." }
122
- @inv_md = type.attribute_metadata(attribute)
123
- unless @inv_md then
124
- raise MetadataError.new("#{@declarer.qp}.#{self} inverse attribute #{type.qp}.#{attribute} not found")
133
+ begin
134
+ @inv_md = type.attribute_metadata(attribute)
135
+ rescue NameError
136
+ raise MetadataError.new("#{@declarer.qp}.#{self} inverse attribute #{type.qp}.#{attribute} not found - #{$!}")
125
137
  end
138
+
139
+ # the inverse of the inverse
126
140
  inv_inv_md = @inv_md.inverse_attribute_metadata
141
+ # If the inverse of the inverse is already set to a different attribute, then raise an exception.
142
+ # Otherwise, it there is an inverse, then set the inverse of the inverse to this attribute.
127
143
  if inv_inv_md then
128
144
  unless inv_inv_md == self then
129
- if @inv_md.inverse == @symbol then
130
- @inv_md.inverse = @symbol
131
- else
132
- raise MetadataError.new("Cannot set #{type.qp}.#{@inv_md} inverse attribute to #{@declarer.qp}.#{self} since it conflicts with existing inverse #{@inv_md.inverse}")
133
- end
145
+ raise MetadataError.new("Cannot set #{type.qp}.#{@inv_md} inverse attribute to #{@declarer.qp}.#{self} since it conflicts with existing inverse #{@inv_md.inverse}")
134
146
  end
135
147
  else
148
+ # set the inverse of the inverse
136
149
  @inv_md.inverse = @symbol
137
- @inv_md.set_flag(:disjoint) if disjoint?
150
+ # if this attribute is disjoint, then so is the inverse.
151
+ @inv_md.qualify(:disjoint) if disjoint?
138
152
  end
153
+
139
154
  # propagate to restrictions
140
- if @restrictions then @restrictions.each { |attr_md| attr_md.inverse = attribute } end
155
+ if @restrictions then @restrictions.each { |attr_md| attr_md.restrict_inverse_type } end
141
156
  end
142
157
 
143
158
  # @return [AttributeMetadata, nil] the metadata for the {#inverse} attribute, if any
@@ -197,7 +212,7 @@ module CaRuby
197
212
  #
198
213
  # @return [Boolean] whether the attribute references a dependent
199
214
  def dependent?
200
- annotation? or @flags.include?(:dependent)
215
+ @flags.include?(:dependent)
201
216
  end
202
217
 
203
218
  # Returns whether the subject attribute is marked as optional in a create.
@@ -224,39 +239,44 @@ module CaRuby
224
239
  def autogenerated?
225
240
  @flags.include?(:autogenerated)
226
241
  end
227
-
228
- # Returns whether the subject attribute is either an #autogenerated? {#dependent?}
229
- # or {#cascaded?} and marked with the :unfetched flag.
230
- def unfetched_created?
231
- (dependent? and autogenerated?) or (cascaded? and @flags.include?(:unfetched))
242
+
243
+ # Returns whether this attribute must be fetched when a declarer instance is saved.
244
+ # An attribute is a saved fetch attribute if either of the following conditions hold:
245
+ # * it is {#autogenerated?}
246
+ # * it is {#cascaded?} and marked with the +:unfetched+ flag.
247
+ #
248
+ # @return [Boolean] whether the subject attribute must be refetched in order to reflect
249
+ # the database content
250
+ def saved_fetch?
251
+ autogenerated? or (cascaded? and @flags.include?(:unfetched))
232
252
  end
233
253
 
234
254
  # Returns whether the subject attribute is a dependent whose owner does not automatically
235
255
  # cascade application service creation or update to the dependent. It is incumbent upon
236
256
  # CaRuby::Database to cascade the changes.
257
+ #
258
+ # @return [Boolean] whether the attribute is an uncascaded dependent
237
259
  def logical?
238
- annotation? or @flags.include?(:logical)
260
+ @flags.include?(:logical)
239
261
  end
240
262
 
241
- # @return whether the subject attribute is an annotation
242
- def annotation?
243
- false
244
- # TODO - enable when annotation enabled
245
- end
246
-
247
- # @return whether this attribute is derived from another attribute
248
- # This occurs when the attribute value is set by setting another attribute, e.g. if this
263
+ # An attribute is derived if the attribute value is set by setting another attribute, e.g. if this
249
264
  # attribute is the inverse of a dependent owner attribute.
265
+ #
266
+ # @return [Boolean] whether this attribute is derived from another attribute
250
267
  def derived?
251
268
  @flags.include?(:derived) or (dependent? and not inverse.nil?)
252
269
  end
253
270
 
254
- # @return this attribute's inverse attribute if the inverse is a derived attribute, or nil otherwise
271
+ # @return [Boolean] this attribute's inverse attribute if the inverse is a derived attribute, or nil otherwise
255
272
  def derived_inverse
256
273
  @inv_md.to_sym if @inv_md and @inv_md.derived?
257
274
  end
258
275
 
259
- # @return whether the subject attribute is a non-dependent domain attribute
276
+ # An independent attribute is a reference to one or more non-dependent Resource objects.
277
+ # An {#owner?} attribute is independent.
278
+ #
279
+ # @return [Boolean] whether the subject attribute is a non-dependent domain attribute
260
280
  def independent?
261
281
  domain? and not dependent?
262
282
  end
@@ -323,8 +343,9 @@ module CaRuby
323
343
  #
324
344
  # @return [Boolean] whether this attribute is saved in a create or update operation
325
345
  def saved?
326
- java_property? and not @flags.include?(:unsaved) and not proxied_save? and
327
- (nondomain? or cascaded? or not collection? or inverse.nil? or unidirectional_java_dependent? or @flags.include?(:saved))
346
+ @flags.include?(:saved) or
347
+ (java_property? and not @flags.include?(:unsaved) and not proxied_save? and
348
+ (nondomain? or cascaded? or not collection? or inverse.nil? or unidirectional_java_dependent?))
328
349
  end
329
350
 
330
351
  # @return [Boolean] whether this attribute is not {#saved?}
@@ -337,12 +358,6 @@ module CaRuby
337
358
  def proxied_save?
338
359
  domain? and type.method_defined?(:saver_proxy)
339
360
  end
340
-
341
- # Each saved attribute is a saved mergeable attribute unless the :saved_unmergeable flag is set.
342
- # @return [Boolean] whether this attribute can be merged from a save result
343
- def saved_mergeable?
344
- not @flags.include?(:saved_unmergeable)
345
- end
346
361
 
347
362
  # Returns whether this attribute's referents must exist before an instance of the
348
363
  # declarer class can be created. An attribute is a storable prerequisite if it is
@@ -396,7 +411,13 @@ module CaRuby
396
411
  def disjoint?
397
412
  @flags.include?(:disjoint)
398
413
  end
399
-
414
+
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
+
400
421
  # @return [Boolean] whether this attribute is a dependent which does not have a Java inverse owner attribute
401
422
  def unidirectional_java_dependent?
402
423
  # TODO - can this be relaxed to java_unidirectional? i.e. eliminate dependent filter
@@ -420,21 +441,89 @@ module CaRuby
420
441
 
421
442
  alias :qp :to_s
422
443
 
444
+ protected
445
+
446
+ # Duplicates the mutable content as part of a {#deep_copy}.
447
+ def dup_content
448
+ # keep the copied flags but don't share them
449
+ @flags = @flags.dup
450
+ # restrictions are neither shared nor copied
451
+ @restrictions = nil
452
+ end
453
+
454
+ # If there is a current inverse which can be restricted to an attribute in the scope of
455
+ # this metadata's restricted type, then reset the inverse to that attribute.
456
+ def restrict_inverse_type
457
+ return unless @inv_md
458
+ # the current inverse
459
+ attr = inverse
460
+ # the restricted type's metadata for the current inverse
461
+ rstr_inv_md = @type.attribute_metadata(attr)
462
+ # bail if the restricted type delegates to the current inverse metadata
463
+ return if rstr_inv_md == @inv_md
464
+ # clear the inverse
465
+ @inv_md = nil
466
+ # reset the inverse to the restricted attribute
467
+ self.inverse = attr
468
+ end
469
+
423
470
  private
424
-
471
+
472
+ # Creates a copy of this metadata which does not share mutable content.
473
+ def deep_copy
474
+ copy = dup
475
+ copy.dup_content
476
+ copy
477
+ end
478
+
479
+ # Creates a new declarer attribute which restricts this attribute type or flags.
480
+ #
481
+ # @param [Class] declarer the class for which the restriction holds
482
+ # @return [AttributeMetadata] the metadata for the new declarer attribute
483
+ def restrict(declarer)
484
+ unless declarer < self.declarer then
485
+ raise ArgumentError.new("Cannot restrict #{self.declarer.qp}.#{self} to incompatible declarer type #{declarer.qp}")
486
+ end
487
+ copy = deep_copy
488
+ # specialize the copy declarer and type
489
+ copy.declarer = declarer
490
+ # Capture the restriction to propagate modifications to this metadata, esp.
491
+ # adding an inverse.
492
+ @restrictions ||= []
493
+ @restrictions << copy
494
+ copy
495
+ end
496
+
497
+ def clear_inverse
498
+ return unless @inv_md
499
+ logger.debug { "Clearing #{@declarer.qp}.#{self} inverse #{type.qp}.#{inverse}..." }
500
+ inv_inv_md = @inv_md.inverse_attribute_metadata
501
+ @inv_md = nil
502
+ if inv_inv_md then inv_inv_md.inverse = nil end
503
+ logger.debug { "Cleared #{@declarer.qp}.#{self} inverse." }
504
+ end
505
+
425
506
  # @param [Symbol] the flag to set
426
507
  # @raise [ArgumentError] if flag is not supported
427
508
  def set_flag(flag)
428
509
  return if @flags.include?(flag)
429
510
  raise ArgumentError.new("Attribute flag not supported: #{flag}") unless SUPPORTED_FLAGS.include?(flag)
430
511
  @flags << flag
431
- if flag == :owner then
432
- inv_attr = type.dependent_attribute(@declarer)
433
- if inv_attr.nil? then
434
- raise MetadataError.new("#{@declarer.qp} owner attribute #{self} does not have a #{type.qp} dependent inverse")
435
- end
436
- self.inverse = type.dependent_attribute(@declarer)
437
- @flags << :logical if inverse_attribute_metadata.logical?
512
+ case flag
513
+ when :owner then
514
+ if dependent? then
515
+ raise MetadataError.new("#{declarer.qp}.#{self} cannot be set as a #{type.qp} owner since it is already defined as a #{type.qp} dependent")
516
+ end
517
+ inv_attr = type.dependent_attribute(@declarer)
518
+ if inv_attr.nil? then
519
+ raise MetadataError.new("#{@declarer.qp} owner attribute #{self} does not have a #{type.qp} dependent inverse")
520
+ end
521
+ self.inverse = type.dependent_attribute(@declarer)
522
+ if inverse_attribute_metadata.logical? then @flags << :logical end
523
+ when :dependent then
524
+ if owner? then
525
+ raise MetadataError.new("#{declarer.qp}.#{self} cannot be set as a #{type.qp} dependent since it is already defined as a #{type.qp} owner")
526
+ end
438
527
  end
439
528
  end
440
529