caruby-core 1.4.2 → 1.4.3

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