jinx 2.1.1

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 (149) hide show
  1. data/.gitignore +14 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +27 -0
  6. data/History.md +6 -0
  7. data/LEGAL +5 -0
  8. data/LICENSE +22 -0
  9. data/README.md +44 -0
  10. data/Rakefile +41 -0
  11. data/examples/family/README.md +10 -0
  12. data/examples/family/ext/build.xml +35 -0
  13. data/examples/family/ext/src/family/Address.java +68 -0
  14. data/examples/family/ext/src/family/Child.java +24 -0
  15. data/examples/family/ext/src/family/DomainObject.java +26 -0
  16. data/examples/family/ext/src/family/Household.java +36 -0
  17. data/examples/family/ext/src/family/Parent.java +48 -0
  18. data/examples/family/ext/src/family/Person.java +42 -0
  19. data/examples/family/lib/family.rb +15 -0
  20. data/examples/family/lib/family/address.rb +6 -0
  21. data/examples/family/lib/family/domain_object.rb +6 -0
  22. data/examples/family/lib/family/household.rb +6 -0
  23. data/examples/family/lib/family/parent.rb +16 -0
  24. data/examples/family/lib/family/person.rb +6 -0
  25. data/examples/model/README.md +25 -0
  26. data/examples/model/ext/build.xml +35 -0
  27. data/examples/model/ext/src/domain/Child.java +192 -0
  28. data/examples/model/ext/src/domain/Dependent.java +29 -0
  29. data/examples/model/ext/src/domain/DomainObject.java +26 -0
  30. data/examples/model/ext/src/domain/Independent.java +83 -0
  31. data/examples/model/ext/src/domain/Parent.java +129 -0
  32. data/examples/model/ext/src/domain/Person.java +14 -0
  33. data/examples/model/lib/model.rb +13 -0
  34. data/examples/model/lib/model/child.rb +13 -0
  35. data/examples/model/lib/model/domain_object.rb +6 -0
  36. data/examples/model/lib/model/independent.rb +11 -0
  37. data/examples/model/lib/model/parent.rb +17 -0
  38. data/jinx.gemspec +22 -0
  39. data/lib/jinx.rb +3 -0
  40. data/lib/jinx/active_support/README.txt +2 -0
  41. data/lib/jinx/active_support/core_ext/string.rb +7 -0
  42. data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
  43. data/lib/jinx/active_support/inflections.rb +55 -0
  44. data/lib/jinx/active_support/inflector.rb +398 -0
  45. data/lib/jinx/cli/application.rb +36 -0
  46. data/lib/jinx/cli/command.rb +214 -0
  47. data/lib/jinx/helpers/array.rb +108 -0
  48. data/lib/jinx/helpers/boolean.rb +42 -0
  49. data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
  50. data/lib/jinx/helpers/class.rb +149 -0
  51. data/lib/jinx/helpers/collection.rb +33 -0
  52. data/lib/jinx/helpers/collections.rb +11 -0
  53. data/lib/jinx/helpers/collector.rb +20 -0
  54. data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
  55. data/lib/jinx/helpers/enumerable.rb +242 -0
  56. data/lib/jinx/helpers/enumerate.rb +35 -0
  57. data/lib/jinx/helpers/error.rb +15 -0
  58. data/lib/jinx/helpers/file_separator.rb +65 -0
  59. data/lib/jinx/helpers/filter.rb +52 -0
  60. data/lib/jinx/helpers/flattener.rb +38 -0
  61. data/lib/jinx/helpers/hash.rb +12 -0
  62. data/lib/jinx/helpers/hashable.rb +502 -0
  63. data/lib/jinx/helpers/inflector.rb +36 -0
  64. data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
  65. data/lib/jinx/helpers/lazy_hash.rb +44 -0
  66. data/lib/jinx/helpers/log.rb +106 -0
  67. data/lib/jinx/helpers/math.rb +12 -0
  68. data/lib/jinx/helpers/merge.rb +60 -0
  69. data/lib/jinx/helpers/module.rb +18 -0
  70. data/lib/jinx/helpers/multi_enumerator.rb +31 -0
  71. data/lib/jinx/helpers/options.rb +92 -0
  72. data/lib/jinx/helpers/os.rb +19 -0
  73. data/lib/jinx/helpers/partial_order.rb +37 -0
  74. data/lib/jinx/helpers/pretty_print.rb +207 -0
  75. data/lib/jinx/helpers/set.rb +8 -0
  76. data/lib/jinx/helpers/stopwatch.rb +76 -0
  77. data/lib/jinx/helpers/transformer.rb +24 -0
  78. data/lib/jinx/helpers/transitive_closure.rb +55 -0
  79. data/lib/jinx/helpers/uniquifier.rb +50 -0
  80. data/lib/jinx/helpers/validation.rb +33 -0
  81. data/lib/jinx/helpers/visitor.rb +370 -0
  82. data/lib/jinx/import/class_path_modifier.rb +77 -0
  83. data/lib/jinx/import/java.rb +337 -0
  84. data/lib/jinx/importer.rb +240 -0
  85. data/lib/jinx/metadata.rb +155 -0
  86. data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
  87. data/lib/jinx/metadata/dependency.rb +244 -0
  88. data/lib/jinx/metadata/id_alias.rb +23 -0
  89. data/lib/jinx/metadata/introspector.rb +179 -0
  90. data/lib/jinx/metadata/inverse.rb +170 -0
  91. data/lib/jinx/metadata/java_property.rb +169 -0
  92. data/lib/jinx/metadata/propertied.rb +500 -0
  93. data/lib/jinx/metadata/property.rb +401 -0
  94. data/lib/jinx/metadata/property_characteristics.rb +114 -0
  95. data/lib/jinx/resource.rb +862 -0
  96. data/lib/jinx/resource/copy_visitor.rb +36 -0
  97. data/lib/jinx/resource/inversible.rb +90 -0
  98. data/lib/jinx/resource/match_visitor.rb +180 -0
  99. data/lib/jinx/resource/matcher.rb +20 -0
  100. data/lib/jinx/resource/merge_visitor.rb +73 -0
  101. data/lib/jinx/resource/mergeable.rb +185 -0
  102. data/lib/jinx/resource/reference_enumerator.rb +49 -0
  103. data/lib/jinx/resource/reference_path_visitor.rb +38 -0
  104. data/lib/jinx/resource/reference_visitor.rb +55 -0
  105. data/lib/jinx/resource/unique.rb +35 -0
  106. data/lib/jinx/version.rb +3 -0
  107. data/spec/defaults_spec.rb +30 -0
  108. data/spec/definitions/model/alias/child.rb +5 -0
  109. data/spec/definitions/model/base/child.rb +5 -0
  110. data/spec/definitions/model/base/domain_object.rb +5 -0
  111. data/spec/definitions/model/base/independent.rb +5 -0
  112. data/spec/definitions/model/defaults/child.rb +5 -0
  113. data/spec/definitions/model/dependency/child.rb +5 -0
  114. data/spec/definitions/model/dependency/parent.rb +6 -0
  115. data/spec/definitions/model/inverse/child.rb +5 -0
  116. data/spec/definitions/model/inverse/independent.rb +5 -0
  117. data/spec/definitions/model/inverse/parent.rb +5 -0
  118. data/spec/definitions/model/mandatory/child.rb +6 -0
  119. data/spec/dependency_spec.rb +47 -0
  120. data/spec/family_spec.rb +64 -0
  121. data/spec/inverse_spec.rb +53 -0
  122. data/spec/mandatory_spec.rb +43 -0
  123. data/spec/metadata_spec.rb +68 -0
  124. data/spec/resource_spec.rb +30 -0
  125. data/spec/spec_helper.rb +3 -0
  126. data/spec/support/model.rb +19 -0
  127. data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
  128. data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
  129. data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
  130. data/test/fixtures/mixed/ext/build.xml +35 -0
  131. data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
  132. data/test/helper.rb +7 -0
  133. data/test/lib/jinx/command_test.rb +41 -0
  134. data/test/lib/jinx/helpers/boolean_test.rb +27 -0
  135. data/test/lib/jinx/helpers/class_test.rb +60 -0
  136. data/test/lib/jinx/helpers/collections_test.rb +402 -0
  137. data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
  138. data/test/lib/jinx/helpers/inflector_test.rb +11 -0
  139. data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
  140. data/test/lib/jinx/helpers/module_test.rb +24 -0
  141. data/test/lib/jinx/helpers/options_test.rb +66 -0
  142. data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
  143. data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
  144. data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
  145. data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
  146. data/test/lib/jinx/helpers/visitor_test.rb +288 -0
  147. data/test/lib/jinx/import/java_test.rb +78 -0
  148. data/test/lib/jinx/import/mixed_case_test.rb +16 -0
  149. metadata +272 -0
@@ -0,0 +1,155 @@
1
+ require 'jinx/helpers/collections'
2
+ require 'jinx/import/java'
3
+ require 'jinx/metadata/java_property'
4
+ require 'jinx/metadata/introspector'
5
+ require 'jinx/metadata/inverse'
6
+ require 'jinx/metadata/dependency'
7
+
8
+ module Jinx
9
+ # Exception raised if a meta-data setting is missing or invalid.
10
+ class MetadataError < RuntimeError; end
11
+
12
+ # The metadata introspection mix-in for a Java application domain class or interface.
13
+ module Metadata
14
+ include Introspector, Inverse, Dependency
15
+
16
+ # @return [Module] the application domain {Resource} module which introspected this class
17
+ attr_accessor :domain_module
18
+
19
+ # @param [Symbol] attribute the property attribute
20
+ # @param [{Symbol => Object}] opts the property options
21
+ # @option opts [true, Symbol, <Symbol>] :dependent whether this property is a dependent reference
22
+ # qualified by the given {Property} flags
23
+ def property(attribute, *opts)
24
+ return super(attribute) if opts.empty?
25
+ Options.to_hash(*opts).each do |k, v|
26
+ case k
27
+ when :alias then alias_attribute(v, attribute)
28
+ when :default then add_attribute_default(attribute, v)
29
+ when :dependent then add_dependent_attribute(attribute) if v
30
+ when :inverse then set_attribute_inverse(attribute, v)
31
+ when :mandatory then add_mandatory_attribute(attribute) if v
32
+ when :primary_key then add_primary_key_attribute(attribute) if v
33
+ when :secondary_key then add_secondary_key_attribute(attribute) if v
34
+ when :alternate_key then add_alternate_key_attribute(attribute) if v
35
+ when :type then set_attribute_type(attribute, v)
36
+ else qualify_attribute(attribute, k) if v
37
+ end
38
+ end
39
+ end
40
+
41
+ # @return [Class, nil] the domain type for attribute, or nil if attribute is not a domain attribute
42
+ def domain_type(attribute)
43
+ prop = property(attribute)
44
+ prop.type if prop.domain?
45
+ end
46
+
47
+ # Returns an empty value for the given attribute.
48
+ # * If this class is not abstract, then the empty value is the initialized value.
49
+ # * Otherwise, if the attribute is a Java primitive number then zero.
50
+ # * Otherwise, if the attribute is a Java primitive boolean then +false+.
51
+ # * Otherwise, the empty value is nil.
52
+ #
53
+ # @param [Symbol] attribute the target attribute
54
+ # @return [Numeric, Boolean, Enumerable, nil] the empty attribute value
55
+ def empty_value(attribute)
56
+ if abstract? then
57
+ prop = property(attribute)
58
+ # the Java attribute type
59
+ jtype = prop.property_descriptor.attribute_type if JavaProperty === prop
60
+ # A primitive is either a boolean or a number (String is not primitive).
61
+ if jtype and jtype.primitive? then
62
+ type.name == 'boolean' ? false : 0
63
+ end
64
+ else
65
+ # Since this class is not abstract, create a prototype instance on demand and make
66
+ # a copy of the initialized collection value from that instance.
67
+ @prototype ||= new
68
+ value = @prototype.send(attribute) || return
69
+ value.class.new
70
+ end
71
+ end
72
+
73
+ # Prints this classifier's content to the log.
74
+ def pretty_print(q)
75
+ map = pretty_print_attribute_hash.delete_if { |k, v| v.nil_or_empty? }
76
+ # one indented line per entry, all but the last line ending in a comma
77
+ content = map.map { |label, value| " #{label}=>#{format_print_value(value)}" }.join(",\n")
78
+ # print the content to the log
79
+ q.text("#{qp} structure:\n#{content}")
80
+ end
81
+
82
+ private
83
+
84
+ # A proc to print the unqualified class name.
85
+ # @private
86
+ TYPE_PRINTER = PrintWrapper.new { |type| type.qp }
87
+
88
+ # A proc to print the property descriptor name.
89
+ # @private
90
+ PROP_DESC_PRINTER = PrintWrapper.new { |pd| pd.name }
91
+
92
+ # @param [Property] the property to print
93
+ # @return [<Symbol>] the flags to modify the property
94
+ def pretty_print_attribute_flags(prop)
95
+ flags = []
96
+ flags << :disjoint if prop.disjoint?
97
+ flags
98
+ end
99
+
100
+ # @return [{String => <Symbol>}] the attributes to print
101
+ def pretty_print_attribute_hash
102
+ # the Java property descriptors
103
+ pds = java_attributes.wrap { |pa| property(pa).property_descriptor }
104
+ # the display label => properties printer
105
+ prop_prn = pds.wrap { |pd| PROP_DESC_PRINTER.wrap(pd) }
106
+ # the Java attributes
107
+ prop_syms = pds.map { |pd| pd.name.to_sym }.to_set
108
+ # the attribute aliases
109
+ aliases = @alias_std_prop_map.keys - attributes.to_a - prop_syms
110
+ alias_hash = aliases.to_compact_hash { |aliaz| @alias_std_prop_map[aliaz] }
111
+ # the dependent attributes printer
112
+ dep_prn_wrapper = dependent_attributes_print_wrapper
113
+ dep_prn = dependent_attributes.wrap { |pa| dep_prn_wrapper.wrap(property(pa)) }
114
+ # the owner classes printer
115
+ own_prn = owners.wrap { |type| TYPE_PRINTER.wrap(type) }
116
+ # the inverse attributes printer
117
+ inv_prn = @attributes.to_compact_hash do |pa|
118
+ prop = property(pa)
119
+ "#{prop.type.qp}.#{prop.inverse}" if prop.inverse
120
+ end
121
+ # the domain attribute printer
122
+ dom_prn = domain_attributes.to_compact_hash { |pa| domain_type(pa).qp }
123
+ # the description => printable hash
124
+ {
125
+ 'Java attributes' => prop_prn,
126
+ 'standard attributes' => attributes,
127
+ 'aliases to standard attributes' => alias_hash,
128
+ 'secondary key' => secondary_key_attributes,
129
+ 'mandatory attributes' => mandatory_attributes,
130
+ 'domain attributes' => dom_prn,
131
+ 'owners' => own_prn,
132
+ 'owner attributes' => owner_attributes,
133
+ 'inverse attributes' => inv_prn,
134
+ 'dependent attributes' => dep_prn,
135
+ 'default values' => defaults
136
+ }
137
+ end
138
+
139
+ # @return [PrintWrapper] a proc to print the dependent attributes
140
+ def dependent_attributes_print_wrapper
141
+ PrintWrapper.new do |prop|
142
+ flags = pretty_print_attribute_flags(prop)
143
+ flags.empty? ? "#{prop}" : "#{prop}(#{flags.join(',')})"
144
+ end
145
+ end
146
+
147
+ def format_print_value(value)
148
+ case value
149
+ when String then value
150
+ when Class then value.qp
151
+ else value.pp_s(:single_line)
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,73 @@
1
+ module Jinx
2
+ # A filter on the standard attribute symbol => metadata hash that yields
3
+ # each attribute which satisfies the attribute metadata condition.
4
+ class AttributeEnumerator
5
+ include Enumerable
6
+
7
+ # @param [{Symbol => Property}] hash the attribute symbol => metadata hash
8
+ # @yield [prop] optional condition which determines whether the attribute is
9
+ # selected (default is all attributes)
10
+ # @yieldparam [Property] the metadata for the standard attribute
11
+ # @raise [ArgumentError] if a parameter is missing
12
+ def initialize(hash, &filter)
13
+ Jinx.fail(ArgumentError, "Attribute filter missing hash argument") if hash.nil?
14
+ @hash = hash
15
+ @filter = block_given? ? filter : Proc.new { true }
16
+ end
17
+
18
+ # @yield [attribute, prop] the block to apply to the filtered attribute metadata and attribute
19
+ # @yieldparam [Symbol] attribute the attribute
20
+ # @yieldparam [Property] prop the attribute metadata
21
+ def each_pair
22
+ @hash.each { |pa, prop| yield(pa, prop) if @filter.call(prop) }
23
+ end
24
+
25
+ # @return [<(Symbol, Property)>] the (symbol, attribute) enumerator
26
+ def enum_pairs
27
+ enum_for(:each_pair)
28
+ end
29
+
30
+ # @yield [attribute] block to apply to each filtered attribute
31
+ # @yieldparam [Symbol] the attribute which satisfies the filter condition
32
+ def each_attribute(&block)
33
+ each_pair { |pa, prop| yield(pa) }
34
+ end
35
+
36
+ alias :each :each_attribute
37
+
38
+ # @yield [prop] the block to apply to the filtered attribute metadata
39
+ # @yieldparam [Property] prop the attribute metadata
40
+ def each_property
41
+ each_pair { |pa, prop| yield(prop) }
42
+ end
43
+
44
+ # @return [<Property>] the property enumerator
45
+ def properties
46
+ @prop_enum ||= enum_for(:each_property)
47
+ end
48
+
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
+ # @yield [prop] the block to apply to the attribute metadata
58
+ # @yieldparam [Property] prop the attribute metadata
59
+ # @return [Symbol] the first attribute whose metadata satisfies the block
60
+ def detect_with_property
61
+ each_pair { |pa, prop| return pa if yield(prop) }
62
+ nil
63
+ end
64
+
65
+ # @yield [prop] the attribute selection filter
66
+ # @yieldparam [Property] prop the candidate attribute metadata
67
+ # @return [AttributeEnumerator] a new eumerator which applies the filter block given to this
68
+ # method with the Property enumerated by this enumerator
69
+ def compose
70
+ AttributeEnumerator.new(@hash) { |prop| @filter.call(prop) and yield(prop) }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,244 @@
1
+ require 'jinx/helpers/validation'
2
+
3
+ module Jinx
4
+ # Metadata mix-in to capture Resource dependency.
5
+ module Dependency
6
+ # @return [<Class>] the owner classes
7
+ attr_reader :owners
8
+
9
+ # @return [<Symbol>] the owner reference attributes
10
+ attr_reader :owner_attributes
11
+
12
+ # Adds the given attribute as a dependent.
13
+ #
14
+ # If the attribute inverse is not a collection, then the attribute writer
15
+ # is modified to delegate to the dependent owner writer. This enforces
16
+ # referential integrity by ensuring that the following post-condition holds:
17
+ # * _owner_._attribute_._inverse_ == _owner_
18
+ # where:
19
+ # * _owner_ is an instance this attribute's declaring class
20
+ # * _inverse_ is the owner inverse attribute defined in the dependent class
21
+ #
22
+ # @param [Symbol] attribute the dependent to add
23
+ # @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}..." }
27
+ 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:
32
+ # 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}." }
35
+ end
36
+
37
+ # @return [Boolean] whether this class depends on an owner
38
+ def dependent?
39
+ not owners.empty?
40
+ end
41
+
42
+ # @param [Class] other the class to check
43
+ # @param [Boolean] recursive whether to check if this class depends on a dependent
44
+ # of the other class
45
+ # @return [Boolean] whether this class depends on the other class
46
+ def depends_on?(other, recursive=false)
47
+ owners.detect do |owner|
48
+ other <= owner or (recursive and depends_on_recursive?(owner, other))
49
+ end
50
+ end
51
+
52
+ # @param [Class] klass the dependent type
53
+ # @return [Symbol, nil] the attribute which references the dependent type, or nil if none
54
+ def dependent_attribute(klass)
55
+ most_specific_domain_attribute(klass, dependent_attributes)
56
+ end
57
+
58
+ # @return [<Property>] the owner properties
59
+ def owner_properties
60
+ @ops ||= create_owner_properties_enumerator
61
+ end
62
+
63
+ # @return [<Symbol>] this class's owner attributes
64
+ def owner_attributes
65
+ @oas ||= owner_properties.transform { |op| op.attribute }
66
+ end
67
+
68
+ # @return [Boolean] whether this {Resource} class is dependent and reference its owners
69
+ def bidirectional_dependent?
70
+ dependent? and not owner_attributes.empty?
71
+ end
72
+
73
+ # @return [<Class>] this class's dependent types
74
+ def dependents
75
+ dependent_attributes.wrap { |da| da.type }
76
+ end
77
+
78
+ # @return [<Class>] this class's owner types
79
+ def owners
80
+ @owners ||= Enumerable::Enumerator.new(owner_property_hash, :each_key)
81
+ end
82
+
83
+ # @return [Property, nil] the sole owner attribute metadata of this class, or nil if there
84
+ # is not exactly one owner
85
+ def owner_property
86
+ props = owner_properties
87
+ props.first if props.size == 1
88
+ end
89
+
90
+ # @return [Symbol, nil] the sole owner attribute of this class, or nil if there
91
+ # is not exactly one owner
92
+ def owner_attribute
93
+ prop = owner_property || return
94
+ prop.attribute
95
+ end
96
+
97
+ # @return [Class, nil] the sole owner type of this class, or nil if there
98
+ # is not exactly one owner
99
+ def owner_type
100
+ prop = owner_property || return
101
+ prop.type
102
+ end
103
+
104
+ protected
105
+
106
+ # Adds the given owner class to this dependent class.
107
+ # This method must be called before any dependent attribute is accessed.
108
+ # If the attribute is given, then the attribute inverse is set.
109
+ # Otherwise, if there is not already an owner attribute, then a new owner attribute is created.
110
+ # The name of the new attribute is the lower-case demodulized owner class name.
111
+ #
112
+ # @param [Class] the owner class
113
+ # @param [Symbol] inverse the owner -> dependent attribute
114
+ # @param [Symbol, nil] attribute the dependent -> owner attribute, if known
115
+ # @raise [ValidationError] if the inverse is nil
116
+ def add_owner(klass, inverse, attribute=nil)
117
+ if inverse.nil? then
118
+ Jinx.fail(ValidationError, "Owner #{klass.qp} missing dependent attribute for dependent #{qp}")
119
+ end
120
+ logger.debug { "Adding #{qp} owner #{klass.qp}#{' attribute ' + attribute.to_s if attribute} with inverse #{inverse}..." }
121
+ if @owner_prop_hash then
122
+ Jinx.fail(MetadataError, "Can't add #{qp} owner #{klass.qp} after dependencies have been accessed")
123
+ end
124
+
125
+ # detect the owner attribute, if necessary
126
+ attribute ||= detect_owner_attribute(klass, inverse)
127
+ prop = property(attribute) if attribute
128
+ # Add the owner class => attribute entry.
129
+ # The attribute is nil if the dependency is unidirectional, i.e. there is an owner class which
130
+ # references this class via a dependency attribute but there is no inverse owner attribute.
131
+ local_owner_property_hash[klass] = prop
132
+ # If the dependency is unidirectional, then our job is done.
133
+ if attribute.nil? then
134
+ logger.debug { "#{qp} owner #{klass.qp} has unidirectional inverse #{inverse}." }
135
+ return
136
+ end
137
+
138
+ # Bi-directional: add the owner property
139
+ local_owner_properties << prop
140
+ # set the inverse if necessary
141
+ unless prop.inverse then
142
+ set_attribute_inverse(attribute, inverse)
143
+ end
144
+ # set the owner flag if necessary
145
+ unless prop.owner? then prop.qualify(:owner) end
146
+
147
+ # Redefine the writer method to warn when changing the owner.
148
+ rdr, wtr = prop.accessors
149
+ redefine_method(wtr) do |old_wtr|
150
+ lambda do |ref|
151
+ prev = send(rdr)
152
+ send(old_wtr, ref)
153
+ if prev and prev != ref then
154
+ if ref.nil? then
155
+ logger.warn("Unset the #{self} owner #{attribute} #{prev}.")
156
+ elsif ref.key != prev.key then
157
+ logger.warn("Reset the #{self} owner #{attribute} from #{prev} to #{ref}.")
158
+ end
159
+ end
160
+ ref
161
+ end
162
+ end
163
+ logger.debug { "Injected owner change warning into #{qp}.#{attribute} writer method #{wtr}." }
164
+ logger.debug { "#{qp} owner #{klass.qp} attribute is #{attribute} with inverse #{inverse}." }
165
+ end
166
+
167
+ # Adds the given attribute as an owner. This method is called when a new attribute is added that
168
+ # references an existing owner.
169
+ #
170
+ # @param [Symbol] attribute the owner attribute
171
+ def add_owner_attribute(attribute)
172
+ prop = property(attribute)
173
+ otype = prop.type
174
+ hash = local_owner_property_hash
175
+ if hash.include?(otype) then
176
+ oa = hash[otype]
177
+ unless oa.nil? then
178
+ Jinx.fail(MetadataError, "Cannot set #{qp} owner attribute to #{attribute} since it is already set to #{oa}")
179
+ end
180
+ hash[otype] = prop
181
+ else
182
+ add_owner(otype, prop.inverse, attribute)
183
+ end
184
+ end
185
+
186
+ # @return [{Class => Property}] this class's owner type => property hash
187
+ def owner_property_hash
188
+ @op_hash ||= create_owner_property_hash
189
+ end
190
+
191
+ private
192
+
193
+ # @param [Class] klass the class to check
194
+ # @param [Boolean] recursive whether to check whether this class depends on a dependent
195
+ # of the other class
196
+ # @return [Boolean] whether the owner class depends on the other class
197
+ def depends_on_recursive?(klass, other)
198
+ klass != self and klass.owners.any? { |owner| owner.depends_on?(other, true) }
199
+ end
200
+
201
+ # @param [<Symbol>] attributes the order in which the effective owner attribute should be determined
202
+ def order_owner_attributes(*attributes)
203
+ @ops = @ops_local = attributes.map { |oa| property(oa) }
204
+ end
205
+
206
+ def local_owner_property_hash
207
+ @local_oa_hash ||= {}
208
+ end
209
+
210
+ # @return [{Class => Property}] a new owner type => attribute hash
211
+ def create_owner_property_hash
212
+ local = local_owner_property_hash
213
+ Class === self && superclass < Resource ? local.union(superclass.owner_property_hash) : local
214
+ end
215
+
216
+ # @return [<Property>] the owner properties defined in this class
217
+ def local_owner_properties
218
+ @ops_local ||= []
219
+ end
220
+
221
+ # @return [<Property>] the owner properties defined in the class hierarchy
222
+ def create_owner_properties_enumerator
223
+ local = local_owner_properties
224
+ Class === self && superclass < Resource ? local.union(superclass.owner_properties) : local
225
+ end
226
+
227
+ # Returns the attribute which references the owner. The owner attribute is the inverse
228
+ # of the given owner class inverse attribute, if it exists. Otherwise, the owner
229
+ # attribute is inferred by #{Inverse#detect_inverse_attribute}.
230
+
231
+ # @param klass (see #add_owner)
232
+ # @param [Symbol] inverse the owner -> dependent attribute
233
+ # @return [Symbol, nil] this class's owner attribute
234
+ def detect_owner_attribute(klass, inverse)
235
+ ia = klass.property(inverse).inverse || detect_inverse_attribute(klass)
236
+ if ia then
237
+ logger.debug { "#{qp} reference to owner #{klass.qp} with inverse #{inverse} is #{ia}." }
238
+ else
239
+ logger.debug { "#{qp} reference to owner #{klass.qp} with inverse #{inverse} was not detected." }
240
+ end
241
+ ia
242
+ end
243
+ end
244
+ end