jinx 2.1.1

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