jinx 2.1.3 → 2.1.4

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 (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")