jinx 2.1.3 → 2.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/Gemfile.lock +27 -0
  2. data/History.md +4 -0
  3. data/lib/jinx/helpers/class.rb +16 -12
  4. data/lib/jinx/helpers/collection.rb +277 -29
  5. data/lib/jinx/helpers/collections.rb +35 -2
  6. data/lib/jinx/helpers/conditional_enumerator.rb +2 -2
  7. data/lib/jinx/helpers/filter.rb +8 -2
  8. data/lib/jinx/helpers/flattener.rb +2 -2
  9. data/lib/jinx/helpers/hash.rb +3 -2
  10. data/lib/jinx/helpers/{hashable.rb → hasher.rb} +125 -77
  11. data/lib/jinx/helpers/module.rb +1 -1
  12. data/lib/jinx/helpers/multi_enumerator.rb +25 -9
  13. data/lib/jinx/helpers/options.rb +4 -3
  14. data/lib/jinx/helpers/partial_order.rb +16 -8
  15. data/lib/jinx/helpers/pretty_print.rb +14 -4
  16. data/lib/jinx/helpers/transformer.rb +3 -1
  17. data/lib/jinx/helpers/transitive_closure.rb +3 -3
  18. data/lib/jinx/helpers/visitor.rb +33 -42
  19. data/lib/jinx/import/java.rb +40 -27
  20. data/lib/jinx/importer.rb +86 -33
  21. data/lib/jinx/metadata/attribute_enumerator.rb +5 -11
  22. data/lib/jinx/metadata/dependency.rb +65 -30
  23. data/lib/jinx/metadata/id_alias.rb +1 -0
  24. data/lib/jinx/metadata/introspector.rb +21 -9
  25. data/lib/jinx/metadata/inverse.rb +14 -11
  26. data/lib/jinx/metadata/java_property.rb +15 -26
  27. data/lib/jinx/metadata/propertied.rb +80 -19
  28. data/lib/jinx/metadata/property.rb +13 -8
  29. data/lib/jinx/metadata/property_characteristics.rb +2 -2
  30. data/lib/jinx/resource.rb +62 -32
  31. data/lib/jinx/resource/inversible.rb +4 -0
  32. data/lib/jinx/resource/match_visitor.rb +0 -1
  33. data/lib/jinx/resource/mergeable.rb +16 -6
  34. data/lib/jinx/resource/reference_enumerator.rb +1 -2
  35. data/lib/jinx/version.rb +1 -1
  36. data/test/lib/jinx/helpers/collections_test.rb +29 -14
  37. data/test/lib/jinx/helpers/visitor_test.rb +7 -20
  38. data/test/lib/jinx/import/mixed_case_test.rb +17 -3
  39. metadata +4 -4
  40. data/lib/jinx/helpers/enumerable.rb +0 -245
@@ -1,8 +1,10 @@
1
+ require 'jinx/helpers/collection'
2
+
1
3
  module Jinx
2
4
  # A filter on the standard attribute symbol => metadata hash that yields
3
5
  # each attribute which satisfies the attribute metadata condition.
4
6
  class AttributeEnumerator
5
- include Enumerable
7
+ include Enumerable, Collection
6
8
 
7
9
  # @param [{Symbol => Property}] hash the attribute symbol => metadata hash
8
10
  # @yield [prop] optional condition which determines whether the attribute is
@@ -46,18 +48,10 @@ module Jinx
46
48
  @prop_enum ||= enum_for(:each_property)
47
49
  end
48
50
 
49
- # @yield [attribute] the block to apply to the attribute
50
- # @yieldparam [Symbol] attribute the attribute to detect
51
- # @return [Property] the first attribute metadata whose attribute satisfies the block
52
- def detect_property
53
- each_pair { |pa, prop| return prop if yield(pa) }
54
- nil
55
- end
56
-
57
51
  # @yield [prop] the block to apply to the attribute metadata
58
52
  # @yieldparam [Property] prop the attribute metadata
59
53
  # @return [Symbol] the first attribute whose metadata satisfies the block
60
- def detect_with_property
54
+ def detect_attribute_with_property
61
55
  each_pair { |pa, prop| return pa if yield(prop) }
62
56
  nil
63
57
  end
@@ -67,7 +61,7 @@ module Jinx
67
61
  # @return [AttributeEnumerator] a new eumerator which applies the filter block given to this
68
62
  # method with the Property enumerated by this enumerator
69
63
  def compose
70
- AttributeEnumerator.new(@hash) { |prop| @filter.call(prop) and yield(prop) }
64
+ AttributeEnumerator.new(@hash) { |prop| yield(prop) if @filter.call(prop) }
71
65
  end
72
66
  end
73
67
  end
@@ -3,35 +3,44 @@ require 'jinx/helpers/validation'
3
3
  module Jinx
4
4
  # Metadata mix-in to capture Resource dependency.
5
5
  module Dependency
6
- # @return [<Class>] the owner classes
7
- attr_reader :owners
8
-
9
6
  # @return [<Symbol>] the owner reference attributes
10
7
  attr_reader :owner_attributes
11
8
 
12
- # Adds the given attribute as a dependent.
9
+ # Adds the given property as a dependent.
13
10
  #
14
- # If the attribute inverse is not a collection, then the attribute writer
11
+ # If the property inverse is not a collection, then the property writer
15
12
  # is modified to delegate to the dependent owner writer. This enforces
16
13
  # referential integrity by ensuring that the following post-condition holds:
17
14
  # * _owner_._attribute_._inverse_ == _owner_
18
15
  # where:
19
- # * _owner_ is an instance this attribute's declaring class
16
+ # * _owner_ is an instance this property's declaring class
20
17
  # * _inverse_ is the owner inverse attribute defined in the dependent class
21
18
  #
22
- # @param [Symbol] attribute the dependent to add
19
+ # @param [Property] property the dependent to add
23
20
  # @param [<Symbol>] flags the attribute qualifier flags
24
- def add_dependent_attribute(attribute, *flags)
25
- prop = property(attribute)
26
- logger.debug { "Marking #{qp}.#{attribute} as a dependent attribute of type #{prop.type.qp}..." }
21
+ def add_dependent_property(property, *flags)
22
+ logger.debug { "Marking #{qp}.#{property} as a dependent attribute of type #{property.type.qp}..." }
27
23
  flags << :dependent unless flags.include?(:dependent)
28
- prop.qualify(*flags)
29
- inverse = prop.inverse
30
- inv_type = prop.type
31
- # example: Parent.add_dependent_attribute(:children) with inverse :parent calls the following:
24
+ property.qualify(*flags)
25
+ inv = property.inverse
26
+ inv_type = property.type
27
+ # example: Parent.add_dependent_property(child_prop) with inverse :parent calls the following:
32
28
  # Child.add_owner(Parent, :children, :parent)
33
- inv_type.add_owner(self, attribute, inverse)
34
- logger.debug { "Marked #{qp}.#{attribute} as a dependent attribute with inverse #{inv_type.qp}#{inverse}." }
29
+ inv_type.add_owner(self, property.attribute, inv)
30
+ if inv then
31
+ logger.debug "Marked #{qp}.#{property} as a dependent attribute with inverse #{inv_type.qp}.#{inv}."
32
+ else
33
+ logger.debug "Marked #{qp}.#{property} as a uni-directional dependent attribute."
34
+ end
35
+ end
36
+
37
+ # Adds the given attribute as a dependent.
38
+ #
39
+ # @see #add_dependent_property
40
+ # @param [Symbol] attribute the dependent to add
41
+ # @param (see #add_dependent_property)
42
+ def add_dependent_attribute(attribute, *flags)
43
+ add_dependent_property(property(attribute), *flags)
35
44
  end
36
45
 
37
46
  # @return [Boolean] whether this class depends on an owner
@@ -66,19 +75,40 @@ module Jinx
66
75
  end
67
76
 
68
77
  # @return [Boolean] whether this {Resource} class is dependent and reference its owners
69
- def bidirectional_dependent?
70
- dependent? and not owner_attributes.empty?
78
+ def bidirectional_java_dependent?
79
+ dependent? and owner_properties.any? { |prop| prop.java_property? }
71
80
  end
72
81
 
73
82
  # @return [<Class>] this class's dependent types
74
- def dependents
75
- dependent_attributes.wrap { |da| da.type }
83
+ def dependent_types
84
+ dependent_properties.wrap { |dp| dp.type }
85
+ end
86
+
87
+ # @return [<Property>, nil] the path of this class to the given class through dependent
88
+ # properties, or nil if there is no such path exists
89
+ def dependency_path_to(klass)
90
+ return if klass.owner_types.empty?
91
+ op = klass.owner_properties.detect { |prop| self <= prop.type }
92
+ dp = op.inverse if op
93
+ dp ||= if klass.owner_properties.size < klass.owner_types.size then
94
+ dependent_properties.detect { |prop| klass <= prop.type }
95
+ end
96
+ return [dp] if dp
97
+ dependent_properties.detect_value do |dp|
98
+ next if self <= dp.type
99
+ path = dp.type.dependency_path_to(klass)
100
+ path.unshift(dp) if path
101
+ end
76
102
  end
103
+
104
+ alias :dependents :dependent_types
77
105
 
78
106
  # @return [<Class>] this class's owner types
79
- def owners
107
+ def owner_types
80
108
  @owners ||= Enumerable::Enumerator.new(owner_property_hash, :each_key)
81
109
  end
110
+
111
+ alias :owners :owner_types
82
112
 
83
113
  # @return [Property, nil] the sole owner attribute metadata of this class, or nil if there
84
114
  # is not exactly one owner
@@ -122,29 +152,34 @@ module Jinx
122
152
  raise MetadataError.new("Can't add #{qp} owner #{klass.qp} after dependencies have been accessed")
123
153
  end
124
154
 
125
- # detect the owner attribute, if necessary
155
+ # Detect the owner attribute, if necessary.
126
156
  attribute ||= detect_owner_attribute(klass, inverse)
127
- prop = property(attribute) if attribute
157
+ hash = local_owner_property_hash
158
+ # Guard against a conflicting owner reference attribute.
159
+ if hash[klass] then
160
+ raise MetadataError.new("Cannot set #{qp} owner attribute to #{attribute or 'nil'} since it is already set to #{hash[klass]}")
161
+ end
128
162
  # Add the owner class => attribute entry.
129
163
  # The attribute is nil if the dependency is unidirectional, i.e. there is an owner class which
130
164
  # references this class via a dependency attribute but there is no inverse owner attribute.
131
- local_owner_property_hash[klass] = prop
165
+ prop = property(attribute) if attribute
166
+ hash[klass] = prop
132
167
  # If the dependency is unidirectional, then our job is done.
133
168
  if attribute.nil? then
134
- logger.debug { "#{qp} owner #{klass.qp} has unidirectional inverse #{inverse}." }
169
+ logger.debug { "#{qp} owner #{klass.qp} has unidirectional dependent attribute #{inverse}." }
135
170
  return
136
171
  end
137
172
 
138
- # Bi-directional: add the owner property
173
+ # Bi-directional: add the owner property.
139
174
  local_owner_properties << prop
140
- # set the inverse if necessary
175
+ # Set the inverse if necessary.
141
176
  unless prop.inverse then
142
177
  set_attribute_inverse(attribute, inverse)
143
178
  end
144
- # set the owner flag if necessary
145
- unless prop.owner? then prop.qualify(:owner) end
179
+ # Set the owner flag if necessary.
180
+ prop.qualify(:owner) unless prop.owner?
146
181
 
147
- # Redefine the writer method to warn when changing the owner.
182
+ # Redefine the writer method to issue a warning if the owner is changed.
148
183
  rdr, wtr = prop.accessors
149
184
  redefine_method(wtr) do |old_wtr|
150
185
  lambda do |ref|
@@ -11,6 +11,7 @@ module Jinx
11
11
  def identifier
12
12
  getId
13
13
  end
14
+
14
15
 
15
16
  # Sets the identifier to the given value.
16
17
  # This method delegates to the Java +id+ attribute writer method.
@@ -8,18 +8,32 @@ module Jinx
8
8
  # Meta-data mix-in to infer attribute meta-data from Java properties.
9
9
  module Introspector
10
10
  include Propertied
11
-
12
- # @return [Boolean] whether this class has been introspected
13
- def introspected?
14
- !!@introspected
11
+
12
+ # Introspects the given class, if necessary. Some member of the class hierarchy
13
+ # must first be introspected.
14
+ #
15
+ # @param [Class] klass the class to introspect if necessary
16
+ # @raise [NoSuchMethodError] if the class or an ancestor of the class is not
17
+ # introspected
18
+ def self.ensure_introspected(klass)
19
+ return if klass < Resource and klass.introspected?
20
+ sc = klass.superclass
21
+ ensure_introspected(sc) unless sc == Java::java.lang.Object
22
+ logger.debug { "Introspecting the fetched object class #{klass}..." }
23
+ # Resolving the class name in the context of the domain module
24
+ # introspects the class.
25
+ sc.domain_module.const_get(klass.name.demodulize)
15
26
  end
16
27
 
17
- # Adds an optional {attribute=>value} constructor parameter to this class.
28
+ # Augments the introspected class +new+ method as follows:
29
+ # * Adds an optional {attribute=>value} constructor parameter.
30
+ # * Calls the {Resource#post_initialize} method after initialization.
18
31
  def add_attribute_value_initializer
19
32
  class << self
20
- def new(params=nil)
33
+ def new(opts=nil)
21
34
  obj = super()
22
- obj.merge_attributes(params) if params
35
+ obj.post_initialize
36
+ obj.merge_attributes(opts) if opts
23
37
  obj
24
38
  end
25
39
  end
@@ -52,8 +66,6 @@ module Jinx
52
66
  # Define the standard Java attribute methods.
53
67
  pds.each { |pd| define_java_property(pd) }
54
68
  end
55
- # Mark this class as introspected.
56
- @introspected = true
57
69
  logger.debug { "Introspection of #{qp} metadata complete." }
58
70
  self
59
71
  end
@@ -23,10 +23,13 @@ module Jinx
23
23
  # A domain attribute is recognized as an inverse according to the
24
24
  # {Inverse#detect_inverse_attribute} criterion.
25
25
  #
26
- # @param [Attribute] property the property to check
26
+ # @param [Property] property the property to check
27
+ # @return [Symbol, nil] the inverse attribute, or nil if none was
28
+ # detected
27
29
  def infer_property_inverse(property)
28
30
  inv = property.type.detect_inverse_attribute(self)
29
- if inv then set_attribute_inverse(property.attribute, inv) end
31
+ set_attribute_inverse(property.attribute, inv) if inv
32
+ inv
30
33
  end
31
34
 
32
35
  # Sets the given bi-directional association attribute's inverse.
@@ -63,7 +66,7 @@ module Jinx
63
66
  # If attribute is the one side of a 1:M or non-reflexive 1:1 relation, then add the inverse updater.
64
67
  unless prop.collection? then
65
68
  # Inject adding to the inverse collection into the attribute writer method.
66
- add_inverse_updater(pa, inverse)
69
+ add_inverse_updater(pa)
67
70
  unless prop.type == inv_prop.type or inv_prop.collection? then
68
71
  prop.type.delegate_writer_to_inverse(inverse, pa)
69
72
  end
@@ -151,26 +154,26 @@ module Jinx
151
154
  # @return (see #detect_inverse_attribute)
152
155
  def detect_inverse_attribute_from_candidates(klass, candidates)
153
156
  return if candidates.empty?
154
- # there can be at most one owner attribute per owner.
157
+ # There can be at most one owner attribute per owner.
155
158
  return candidates.first.to_sym if candidates.size == 1
156
- # by convention, if more than one attribute references the owner type,
157
- # then the attribute named after the owner type is the owner attribute
158
- tgt = klass.name[/\w+$/].underscore.to_sym
159
+ # By convention, if more than one attribute references the owner type,
160
+ # then the attribute named after the owner type is the owner attribute.
161
+ tgt = klass.name.demodulize.underscore.to_sym
159
162
  tgt if candidates.detect { |pa| pa == tgt }
160
163
  end
161
164
 
162
165
  # Modifies the given attribute writer method to update the given inverse.
163
166
  #
164
167
  # @param (see #set_attribute_inverse)
165
- def add_inverse_updater(attribute, inverse)
168
+ def add_inverse_updater(attribute)
166
169
  prop = property(attribute)
167
170
  # the reader and writer methods
168
171
  rdr, wtr = prop.accessors
169
- # the inverse atttribute metadata
172
+ # the inverse attribute metadata
170
173
  inv_prop = prop.inverse_property
171
174
  # the inverse attribute reader and writer
172
175
  inv_rdr, inv_wtr = inv_accessors = inv_prop.accessors
173
- # Redefine the writer method to update the inverse by delegating to the inverse
176
+ # Redefine the writer method to update the inverse by delegating to the inverse.
174
177
  redefine_method(wtr) do |old_wtr|
175
178
  # the attribute reader and (superseded) writer
176
179
  accessors = [rdr, old_wtr]
@@ -180,7 +183,7 @@ module Jinx
180
183
  lambda { |other| set_inversible_noncollection_attribute(other, accessors, inv_wtr) }
181
184
  end
182
185
  end
183
- logger.debug { "Injected inverse #{inverse} updater into #{qp}.#{attribute} writer method #{wtr}." }
186
+ logger.debug { "Injected inverse #{inv_prop} updater into #{qp}.#{attribute} writer method #{wtr}." }
184
187
  end
185
188
  end
186
189
  end
@@ -5,6 +5,9 @@ module Jinx
5
5
  # The attribute metadata for an introspected Java property.
6
6
  class JavaProperty < Property
7
7
 
8
+ # This property's Java property descriptor type JRuby class wrapper.
9
+ attr_reader :java_wrapper_class
10
+
8
11
  # This property's Java property descriptor.
9
12
  attr_reader :property_descriptor
10
13
 
@@ -35,21 +38,22 @@ module Jinx
35
38
  # deficient Java introspector does not recognize 'is' prefix for a Boolean property
36
39
  rm = declarer.property_read_method(pd)
37
40
  raise ArgumentError.new("Property does not have a read method: #{declarer.qp}.#{pd.name}") unless rm
38
- reader = rm.name.to_sym
39
- unless declarer.method_defined?(reader) then
40
- reader = "is#{reader.to_s.capitalize_first}".to_sym
41
- unless declarer.method_defined?(reader) then
41
+ rdr = rm.name.to_sym
42
+ unless declarer.method_defined?(rdr) then
43
+ rdr = "is#{rdr.to_s.capitalize_first}".to_sym
44
+ unless declarer.method_defined?(rdr) then
42
45
  raise ArgumentError.new("Reader method not found for #{declarer} property #{pd.name}")
43
46
  end
44
47
  end
45
48
  unless pd.write_method then
46
49
  raise ArgumentError.new("Property does not have a write method: #{declarer.qp}.#{pd.name}")
47
50
  end
48
- writer = pd.write_method.name.to_sym
49
- unless declarer.method_defined?(writer) then
51
+ wtr = pd.write_method.name.to_sym
52
+ unless declarer.method_defined?(wtr) then
50
53
  raise ArgumentError.new("Writer method not found for #{declarer} property #{pd.name}")
51
54
  end
52
- @java_accessors = [reader, writer]
55
+ @java_accessors = [rdr, wtr]
56
+ @java_wrapper_class = Class.to_ruby(pd.property_type)
53
57
  qualify(:collection) if collection_java_class?
54
58
  @type = infer_type
55
59
  end
@@ -97,15 +101,13 @@ module Jinx
97
101
 
98
102
  # @return [Boolean] whether this property's Java type is +Iterable+
99
103
  def collection_java_class?
100
- # the Java property type
101
- ptype = @property_descriptor.property_type
102
- # Test whether the corresponding JRuby wrapper class or module is an Iterable.
103
- Class.to_ruby(ptype) < Java::JavaLang::Iterable
104
+ # Test whether the JRuby wrapper class or module is an Iterable.
105
+ @java_wrapper_class < Java::JavaLang::Iterable
104
106
  end
105
107
 
106
108
  # @return [Class] the type for the specified klass property descriptor pd as described in {#initialize}
107
109
  def infer_type
108
- collection? ? infer_collection_type : infer_non_collection_type
110
+ collection? ? infer_collection_type : @java_wrapper_class
109
111
  end
110
112
 
111
113
  # Returns the domain type for this property's Java Collection property descriptor.
@@ -117,12 +119,6 @@ module Jinx
117
119
  generic_parameter_type or infer_collection_type_from_name or Java::JavaLang::Object
118
120
  end
119
121
 
120
- # @return [Class] this property's Ruby type
121
- def infer_non_collection_type
122
- jtype = @property_descriptor.property_type
123
- Class.to_ruby(jtype)
124
- end
125
-
126
122
  # @return [Class, nil] the domain type of this property's property descriptor Collection generic
127
123
  # type argument, or nil if none
128
124
  def generic_parameter_type
@@ -132,18 +128,11 @@ module Jinx
132
128
  atypes = gtype.actualTypeArguments
133
129
  return unless atypes.size == 1
134
130
  atype = atypes[0]
135
- klass = java_to_ruby_class(atype)
131
+ klass = Class.to_ruby(atype)
136
132
  logger.debug { "Inferred #{declarer.qp} #{self} domain type #{klass.qp} from generic parameter #{atype.name}." } if klass
137
133
  klass
138
134
  end
139
135
 
140
- # @param [Class, String] jtype the Java class or class name
141
- # @return [Class] the corresponding Ruby type
142
- def java_to_ruby_class(jtype)
143
- name = String === jtype ? jtype : jtype.name
144
- Class.to_ruby(name)
145
- end
146
-
147
136
  # Returns the domain type for this property's collection Java property descriptor name.
148
137
  # By convention, Jinx domain collection propertys often begin with a domain type
149
138
  # name and end in 'Collection'. This method strips the Collection suffix and checks
@@ -10,7 +10,7 @@ module Jinx
10
10
  # @return [<Symbol>] this class's attributes
11
11
  attr_reader :attributes
12
12
 
13
- # @return [Hashable] the default attribute => value associations
13
+ # @return [Hasher] the default attribute => value associations
14
14
  attr_reader :defaults
15
15
 
16
16
  # Returns whether this class has an attribute with the given symbol.
@@ -77,6 +77,11 @@ module Jinx
77
77
  @prop_hash.each_value(&block)
78
78
  end
79
79
 
80
+ # @return [<Property>] this domain class's properties
81
+ def properties
82
+ @props ||= enum_for(:each_property)
83
+ end
84
+
80
85
  # @param [Symbol] attribute the property attribute symbol or alias
81
86
  # @return [Property] the corresponding property
82
87
  # @raise [NameError] if the attribute is not recognized
@@ -133,6 +138,11 @@ module Jinx
133
138
  def domain_attributes
134
139
  @dom_flt ||= attribute_filter { |prop| prop.domain? }
135
140
  end
141
+
142
+ # @return [<Property>] the domain properties
143
+ def domain_properties
144
+ domain_attributes.properties
145
+ end
136
146
 
137
147
  # @return [<Symbol>] the non-domain Java attributes
138
148
  def nondomain_attributes
@@ -164,6 +174,12 @@ module Jinx
164
174
  end
165
175
  end
166
176
 
177
+ # @param (see #dependent_attributes)
178
+ # @return [<Property>] the dependent properties
179
+ def dependent_properties(inc_super=true)
180
+ dependent_attributes(inc_super).properties
181
+ end
182
+
167
183
  # @return [<Symbol>] the unidirectional dependent attributes
168
184
  # @see Property#unidirectional?
169
185
  def unidirectional_dependent_attributes
@@ -250,7 +266,48 @@ module Jinx
250
266
  @local_defaults = {}
251
267
  @defaults = append_ancestor_enum(@local_defaults) { |par| par.defaults }
252
268
  end
253
-
269
+
270
+ # Creates a new convenience property in this source class which composes
271
+ # the given property and the other property. The new property symbol is
272
+ # the same as the other property symbol. The new property reader and
273
+ # writer methods delegate to the respective composed property reader and
274
+ # writer methods.
275
+ #
276
+ # @param [Property] property the reference from the source to the intermediary
277
+ # @param [Property] other the reference from the intermediary to the target
278
+ # @yield [target] obtain the intermediary object from the target
279
+ # @yieldparam [Resource] target the target object
280
+ # @return [Property] the new property
281
+ # @raise [ArgumentError] if the other property does not have an inverse
282
+ def compose_property(property, other)
283
+ if other.inverse.nil? then
284
+ raise ArgumentError.new("Can't compose #{qp}.#{property} with inverseless #{other.declarer.qp}.#{other}")
285
+ end
286
+ # the source -> intermediary access methods
287
+ sir, siw = property.accessors
288
+ # the intermediary -> target access methods
289
+ itr, itw = other.accessors
290
+ # the target -> intermediary reader method
291
+ tir = other.inverse
292
+ # The reader composes the source -> intermediary -> target readers.
293
+ define_method(itr) do
294
+ ref = send(sir)
295
+ ref.send(itr) if ref
296
+ end
297
+ # The writer sets the source intermediary to the target intermediary.
298
+ define_method(itw) do |tgt|
299
+ if tgt then
300
+ ref = block_given? ? yield(tgt) : tgt.send(tir)
301
+ raise ArgumentError.new("#{tgt} does not reference a #{other.inverse}") if ref.nil?
302
+ end
303
+ send(siw, ref)
304
+ end
305
+ prop = add_attribute(itr, other.type)
306
+ logger.debug { "Created #{qp}.#{prop} which composes #{qp}.#{property} and #{other.declarer.qp}.#{other}." }
307
+ prop.qualify(:collection) if other.collection?
308
+ prop
309
+ end
310
+
254
311
  # @param (see #add_attribute)
255
312
  # @return (see #add_attribute)
256
313
  def create_nonjava_property(attribute, type, *flags)
@@ -279,14 +336,6 @@ module Jinx
279
336
  end
280
337
  end
281
338
 
282
- # Detects the first attribute with the given type.
283
- #
284
- # @param [Class] klass the target attribute type
285
- # @return [Symbol, nil] the attribute with the given type
286
- def detect_attribute_with_type(klass)
287
- property_hash.detect_key_with_value { |prop| prop.type == klass }
288
- end
289
-
290
339
  # Creates the given attribute alias. If the attribute metadata is registered with this class, then
291
340
  # this method overrides +Class.alias_attribute+ to create a new alias reader (writer) method
292
341
  # which delegates to the attribute reader (writer, resp.). This aliasing mechanism differs from
@@ -398,18 +447,25 @@ module Jinx
398
447
 
399
448
  # Marks the given attribute with flags supported by {Property#qualify}.
400
449
  #
401
- # @param [Symbol] attribute the attribute to qualify
450
+ # @param [Property] property the property to qualify
402
451
  # @param [{Symbol => Object}] the flags to apply to the restricted attribute
403
- def qualify_attribute(attribute, *flags)
404
- prop = property(attribute)
405
- if prop.declarer == self then
406
- prop.qualify(*flags)
452
+ def qualify_property(property, *flags)
453
+ if property.declarer == self then
454
+ property.qualify(*flags)
407
455
  else
408
- logger.debug { "Restricting #{prop.declarer.qp}.#{attribute} to #{qp} with additional flags #{flags.to_series}" }
409
- prop.restrict_flags(self, *flags)
456
+ logger.debug { "Restricting #{property.declarer.qp}.#{property} to #{qp} with additional flags #{flags.to_series}" }
457
+ property.restrict_flags(self, *flags)
410
458
  end
411
459
  end
412
460
 
461
+ # Convenience method which delegates to {#qualify_property}.
462
+ #
463
+ # @param [Symbol] attribute the attribute to qualify
464
+ # @param [{Symbol => Object}] (see #qualify_property)
465
+ def qualify_attribute(attribute, *flags)
466
+ qualify_property(property(attribute), *flags)
467
+ end
468
+
413
469
  # Removes the given attribute from this Resource.
414
470
  # An attribute declared in a superclass Resource is hidden from this Resource but retained in
415
471
  # the declaring Resource.
@@ -431,11 +487,16 @@ module Jinx
431
487
  anc_alias_hash = @alias_std_prop_map.components[1]
432
488
  @alias_std_prop_map.components[1] = anc_alias_hash.filter_on_key { |pa| pa != attribute }
433
489
  end
490
+ logger.debug { "Removed the #{qp} #{attribute} property." }
434
491
  end
435
492
 
436
493
  # @param [Property] the property to add
437
494
  def add_property(property)
438
495
  pa = property.attribute
496
+ # Guard against redundant property
497
+ if @local_prop_hash.has_key?(pa) then
498
+ raise ArgumentError.new("#{self} property already exists: #{pa}")
499
+ end
439
500
  @local_prop_hash[pa] = property
440
501
  # map the attribute symbol to itself in the alias map
441
502
  @local_std_prop_hash[pa] = pa
@@ -451,13 +512,13 @@ module Jinx
451
512
  end
452
513
 
453
514
  # Appends to the given enumerable the result of evaluating the block given to this method
454
- # on the superclass, if the superclass is in the same parent module as this class.
515
+ # on the superclass, if the superclass is also a {Resource} class.
455
516
  #
456
517
  # @param [Enumerable] enum the base collection
457
518
  # @return [Enumerable] the {Enumerable#union} of the base collection with the superclass
458
519
  # collection, if applicable
459
520
  def append_ancestor_enum(enum)
460
- return enum unless Class === self and superclass.parent_module == parent_module
521
+ return enum unless Class === self and superclass < Resource and superclass.introspected?
461
522
  anc_enum = yield superclass
462
523
  if anc_enum.nil? then
463
524
  raise MetadataError.new("#{qp} superclass #{superclass.qp} does not have required metadata")