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,23 @@
1
+ module Jinx
2
+ # Mix-in for Java classes which have an +id+ attribute.
3
+ # Since +id+ is a reserved Ruby method, this mix-in defines an +identifier+ attribute
4
+ # which fronts the +id+ attribute. This mix-in should be included by any JRuby wrapper
5
+ # class for a Java class or interface which implements an +id+ property.
6
+ module IdAlias
7
+ # Returns the identifier.
8
+ # This method delegates to the Java +id+ attribute reader method.
9
+ #
10
+ # @return [Integer] the identifier value
11
+ def identifier
12
+ getId
13
+ end
14
+
15
+ # Sets the identifier to the given value.
16
+ # This method delegates to the Java +id+ attribute writer method.
17
+ #
18
+ # @param [Integer] value the value to set
19
+ def identifier=(value)
20
+ setId(value)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,179 @@
1
+ require 'jinx/helpers/module'
2
+ require 'jinx/import/java'
3
+ require 'jinx/metadata/propertied'
4
+ require 'jinx/metadata/java_property'
5
+
6
+ module Jinx
7
+ # Meta-data mix-in to infer attribute meta-data from Java properties.
8
+ module Introspector
9
+ include Propertied
10
+
11
+ # @return [Boolean] whether this class has been introspected
12
+ def introspected?
13
+ !!@introspected
14
+ end
15
+
16
+ # Adds an optional {attribute=>value} constructor parameter to this class.
17
+ def add_attribute_value_initializer
18
+ class << self
19
+ def new(opts=nil)
20
+ obj = super()
21
+ obj.merge_attributes(opts) if opts
22
+ obj
23
+ end
24
+ end
25
+ logger.debug { "#{self} is extended with an optional {attribute=>value} constructor parameter." }
26
+ end
27
+
28
+ # Defines the Java attribute access methods, e.g. +study_protocol+ and +studyProtocol+.
29
+ # A boolean attribute is provisioned with an additional reader alias, e.g. +available?+
30
+ # for +is_available+.
31
+ #
32
+ # Each Java property attribute delegates to the Java attribute getter and setter.
33
+ # Each standard attribute delegates to the Java property attribute.
34
+ # Redefining these methods results in a call to the redefined method.
35
+ # This contrasts with a Ruby alias, where the alias remains bound to the
36
+ # original method body.
37
+ def introspect
38
+ # Set up the attribute data structures; delegates to Propertied.
39
+ init_property_classifiers
40
+ logger.debug { "Introspecting #{qp} metadata..." }
41
+ # check for method conflicts
42
+ conflicts = instance_methods(false) & Resource.instance_methods(false)
43
+ unless conflicts.empty? then
44
+ logger.warn("#{self} methods conflict with #{Resource} methods: #{conflicts.qp}")
45
+ end
46
+ # If this is a Java class rather than interface, then define the Java property
47
+ # attributes.
48
+ if Class === self then
49
+ # the Java attributes defined by this class with both a read and a write method
50
+ pds = property_descriptors(false)
51
+ # Define the standard Java attribute methods.
52
+ pds.each { |pd| define_java_property(pd) }
53
+ end
54
+ # Mark this class as introspected.
55
+ @introspected = true
56
+ logger.debug { "Introspection of #{qp} metadata complete." }
57
+ self
58
+ end
59
+
60
+ private
61
+
62
+ # Defines the Java property attribute and standard attribute methods, e.g.
63
+ # +study_protocol+ and +studyProtocol+. A boolean attribute is provisioned
64
+ # with an additional reader alias, e.g. +available?+ for +is_available+.
65
+ #
66
+ # A standard attribute which differs from the property attribute delegates
67
+ # to the property attribute, e.g. +study_protocol+ delegates to +studyProtocol+
68
+ # rather than aliasing +setStudyProtocol+. Redefining these methods results
69
+ # in a call to the redefined method. This contrasts with a Ruby alias,
70
+ # where each attribute alias is bound to the respective attribute reader or
71
+ # writer.
72
+ #
73
+ # @param [Java::PropertyDescriptor] the introspected property descriptor
74
+ def define_java_property(pd)
75
+ if transient?(pd) then
76
+ logger.debug { "Ignoring #{name.demodulize} transient attribute #{pd.name}." }
77
+ return
78
+ end
79
+ # the standard underscore lower-case attributes
80
+ ja = add_java_property(pd).attribute
81
+ # delegate the standard attribute accessors to the attribute accessors
82
+ alias_property_accessors(ja, pd.name)
83
+ # add special wrappers
84
+ wrap_java_property(ja, pd)
85
+ # create Ruby alias for boolean, e.g. alias :empty? for :empty
86
+ if pd.property_type.name[/\w+$/].downcase == 'boolean' then
87
+ # strip leading is_, if any, before appending question mark
88
+ aliaz = ja.to_s[/^(is_)?(\w+)/, 2] << '?'
89
+ delegate_to_attribute(aliaz, ja)
90
+ end
91
+ end
92
+
93
+ # Adds a filter to the attribute access method for the property descriptor pd if it is a String or Date.
94
+ def wrap_java_property(attribute, pd)
95
+ if pd.property_type == Java::JavaLang::String.java_class then
96
+ wrap_java_string_attribute(attribute, pd)
97
+ elsif pd.property_type == Java::JavaUtil::Date.java_class then
98
+ wrap_java_date_attribute(attribute, pd)
99
+ end
100
+ end
101
+
102
+ # Adds a to_s filter to this Class's String attribute access methods.
103
+ def wrap_java_string_attribute(attribute, pd)
104
+ # filter the attribute writer
105
+ awtr = "#{attribute}=".to_sym
106
+ pwtr = pd.write_method.name.to_sym
107
+ define_method(awtr) do |value|
108
+ stdval = value.to_s unless value.nil_or_empty?
109
+ send(pwtr, stdval)
110
+ end
111
+ logger.debug { "Filtered #{qp} #{awtr} method with non-String -> String converter." }
112
+ end
113
+
114
+ # Adds a date parser filter to this Class's Date attribute access methods.
115
+ def wrap_java_date_attribute(attribute, pd)
116
+ # filter the attribute reader
117
+ prdr = pd.read_method.name.to_sym
118
+ define_method(attribute) do
119
+ value = send(prdr)
120
+ Java::JavaUtil::Date === value ? value.to_ruby_date : value
121
+ end
122
+
123
+ # filter the attribute writer
124
+ awtr = "#{attribute}=".to_sym
125
+ pwtr = pd.write_method.name.to_sym
126
+ define_method(awtr) do |value|
127
+ value = Java::JavaUtil::Date.from_ruby_date(value) if ::Date === value
128
+ send(pwtr, value)
129
+ end
130
+
131
+ logger.debug { "Filtered #{qp} #{attribute} and #{awtr} methods with Java Date <-> Ruby Date converter." }
132
+ end
133
+
134
+ # Aliases the methods _aliaz_ and _aliaz=_ to _attribute_ and _attribute=_, resp.,
135
+ # where _attribute_ is the Java attribute name for the attribute.
136
+ def alias_property_accessors(aliaz, attribute)
137
+ # strip the Java reader and writer is/get/set prefix and make a symbol
138
+ prdr, pwtr = property(attribute).property_accessors
139
+ alias_method(aliaz, prdr)
140
+ writer = "#{aliaz}=".to_sym
141
+ alias_method(writer, pwtr)
142
+ end
143
+
144
+ # Makes a standard attribute for the given property descriptor.
145
+ # Adds a camelized Java-like alias to the standard attribute.
146
+ #
147
+ # @param (see #define_java_property)
148
+ # @return [Property] the new property
149
+ def add_java_property(pd)
150
+ # make the attribute metadata
151
+ prop = create_java_property(pd)
152
+ add_property(prop)
153
+ # the property name is an alias for the standard attribute
154
+ pa = prop.attribute
155
+ # the Java property name as an attribute symbol
156
+ ja = pd.name.to_sym
157
+ delegate_to_attribute(ja, pa) unless pa == ja
158
+ prop
159
+ end
160
+
161
+ # @param (see #add_java_property)
162
+ # @return (see #add_java_property)
163
+ def create_java_property(pd)
164
+ JavaProperty.new(pd, self)
165
+ end
166
+
167
+ # Defines methods _aliaz_ and _aliaz=_ which calls the standard _attribute_ and
168
+ # _attribute=_ accessor methods, resp.
169
+ # Calling rather than aliasing the attribute accessor allows the aliaz accessor to
170
+ # reflect a change to the attribute accessor.
171
+ def delegate_to_attribute(aliaz, attribute)
172
+ if aliaz == attribute then Jinx.fail(MetadataError, "Cannot delegate #{self} #{aliaz} to itself.") end
173
+ rdr, wtr = property(attribute).accessors
174
+ define_method(aliaz) { send(rdr) }
175
+ define_method("#{aliaz}=".to_sym) { |value| send(wtr, value) }
176
+ register_property_alias(aliaz, attribute)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,170 @@
1
+ module Jinx
2
+ # Meta-data mix-in to infer and set inverse attributes.
3
+ module Inverse
4
+ # Returns the inverse of the given attribute. If the attribute has an #{Property#inverse_property},
5
+ # then that attribute's inverse is returned. Otherwise, if the attribute is an #{Property#owner?},
6
+ # then the target class dependent attribute which matches this type is returned, if it exists.
7
+ #
8
+ # @param [Property] prop the subject attribute
9
+ # @param [Class, nil] klass the target class
10
+ # @return [Property, nil] the inverse attribute, if any
11
+ def inverse_property(prop, klass=nil)
12
+ inv_prop = prop.inverse_property
13
+ return inv_prop if inv_prop
14
+ if prop.dependent? and klass then
15
+ klass.owner_property_hash.each { |otype, op|
16
+ return op if self <= otype }
17
+ end
18
+ end
19
+
20
+ protected
21
+
22
+ # Infers the inverse of the given property declared by this class.
23
+ # A domain attribute is recognized as an inverse according to the
24
+ # {Inverse#detect_inverse_attribute} criterion.
25
+ #
26
+ # @param [Attribute] property the property to check
27
+ def infer_property_inverse(property)
28
+ inv = property.type.detect_inverse_attribute(self)
29
+ if inv then set_attribute_inverse(property.attribute, inv) end
30
+ end
31
+
32
+ # Sets the given bi-directional association attribute's inverse.
33
+ #
34
+ # @param [Symbol] attribute the subject attribute
35
+ # @param [Symbol] the attribute inverse
36
+ # @raise [TypeError] if the inverse type is incompatible with this Resource
37
+ def set_attribute_inverse(attribute, inverse)
38
+ prop = property(attribute)
39
+ # the standard attribute
40
+ pa = prop.attribute
41
+ # return if inverse is already set
42
+ return if prop.inverse == inverse
43
+ # the default inverse
44
+ inverse ||= prop.type.detect_inverse_attribute(self)
45
+ # If the attribute is not declared by this class, then make a new attribute
46
+ # metadata specialized for this class.
47
+ unless prop.declarer == self then
48
+ prop = restrict_attribute_inverse(prop, inverse)
49
+ end
50
+ logger.debug { "Setting #{qp}.#{pa} inverse to #{inverse}..." }
51
+ # the inverse attribute meta-data
52
+ inv_prop = prop.type.property(inverse)
53
+ # If the attribute is the many side of a 1:M relation, then delegate to the one side.
54
+ if prop.collection? and not inv_prop.collection? then
55
+ return prop.type.set_attribute_inverse(inverse, pa)
56
+ end
57
+ # This class must be the same as or a subclass of the inverse attribute type.
58
+ unless self <= inv_prop.type then
59
+ Jinx.fail(TypeError, "Cannot set #{qp}.#{pa} inverse to #{prop.type.qp}.#{pa} with incompatible type #{inv_prop.type.qp}")
60
+ end
61
+ # Set the inverse in the attribute metadata.
62
+ prop.inverse = inverse
63
+ # If attribute is the one side of a 1:M or non-reflexive 1:1 relation, then add the inverse updater.
64
+ unless prop.collection? then
65
+ # Inject adding to the inverse collection into the attribute writer method.
66
+ add_inverse_updater(pa, inverse)
67
+ unless prop.type == inv_prop.type or inv_prop.collection? then
68
+ prop.type.delegate_writer_to_inverse(inverse, pa)
69
+ end
70
+ end
71
+ logger.debug { "Set #{qp}.#{pa} inverse to #{inverse}." }
72
+ end
73
+
74
+ # Detects an unambiguous attribute which refers to the given referencing class.
75
+ # If there is exactly one attribute with the given return type, then that attribute is chosen.
76
+ # Otherwise, the attribute whose name matches the underscored referencing class name is chosen,
77
+ # if any.
78
+ #
79
+ # @param [Class] klass the referencing class
80
+ # @return [Symbol, nil] the inverse attribute for the given referencing class and inverse,
81
+ # or nil if no owner attribute was detected
82
+ def detect_inverse_attribute(klass)
83
+ # The candidate attributes return the referencing type and don't already have an inverse.
84
+ candidates = domain_attributes.compose { |prop| klass <= prop.type and prop.inverse.nil? }
85
+ pa = detect_inverse_attribute_from_candidates(klass, candidates)
86
+ if pa then
87
+ logger.debug { "#{qp} #{klass.qp} inverse attribute is #{pa}." }
88
+ else
89
+ logger.debug { "#{qp} #{klass.qp} inverse attribute was not detected." }
90
+ end
91
+ pa
92
+ end
93
+
94
+ # Redefines the attribute writer method to delegate to its inverse writer.
95
+ # This is done to enforce inverse integrity.
96
+ #
97
+ # For a +Person+ attribute +account+ with inverse +holder+, this is equivalent to the following:
98
+ # class Person
99
+ # alias :set_account :account=
100
+ # def account=(acct)
101
+ # acct.holder = self if acct
102
+ # set_account(acct)
103
+ # end
104
+ # end
105
+ def delegate_writer_to_inverse(attribute, inverse)
106
+ prop = property(attribute)
107
+ # nothing to do if no inverse
108
+ inv_prop = prop.inverse_property || return
109
+ logger.debug { "Delegating #{qp}.#{attribute} update to the inverse #{prop.type.qp}.#{inv_prop}..." }
110
+ # redefine the write to set the dependent inverse
111
+ redefine_method(prop.writer) do |old_writer|
112
+ # delegate to the Jinx::Resource set_inverse method
113
+ lambda { |dep| set_inverse(dep, old_writer, inv_prop.writer) }
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ # Copies the given attribute metadata from its declarer to this class. The new attribute metadata
120
+ # has the same attribute access methods, but the declarer is this class and the inverse is the
121
+ # given inverse attribute.
122
+ #
123
+ # @param [Property] prop the attribute to copy
124
+ # @param [Symbol] the attribute inverse
125
+ # @return [Property] the copied attribute metadata
126
+ def restrict_attribute_inverse(prop, inverse)
127
+ logger.debug { "Restricting #{prop.declarer.qp}.#{prop} to #{qp} with inverse #{inverse}..." }
128
+ rst_prop = prop.restrict(self, :inverse => inverse)
129
+ logger.debug { "Restricted #{prop.declarer.qp}.#{prop} to #{qp} with inverse #{inverse}." }
130
+ rst_prop
131
+ end
132
+
133
+ # @param klass (see #detect_inverse_attribute)
134
+ # @param [<Symbol>] candidates the attributes constrained to the target type
135
+ # @return (see #detect_inverse_attribute)
136
+ def detect_inverse_attribute_from_candidates(klass, candidates)
137
+ return if candidates.empty?
138
+ # there can be at most one owner attribute per owner.
139
+ return candidates.first.to_sym if candidates.size == 1
140
+ # by convention, if more than one attribute references the owner type,
141
+ # then the attribute named after the owner type is the owner attribute
142
+ tgt = klass.name[/\w+$/].underscore.to_sym
143
+ tgt if candidates.detect { |pa| pa == tgt }
144
+ end
145
+
146
+ # Modifies the given attribute writer method to update the given inverse.
147
+ #
148
+ # @param (see #set_attribute_inverse)
149
+ def add_inverse_updater(attribute, inverse)
150
+ prop = property(attribute)
151
+ # the reader and writer methods
152
+ rdr, wtr = prop.accessors
153
+ # the inverse atttribute metadata
154
+ inv_prop = prop.inverse_property
155
+ # the inverse attribute reader and writer
156
+ inv_rdr, inv_wtr = inv_accessors = inv_prop.accessors
157
+ # Redefine the writer method to update the inverse by delegating to the inverse
158
+ redefine_method(wtr) do |old_wtr|
159
+ # the attribute reader and (superseded) writer
160
+ accessors = [rdr, old_wtr]
161
+ if inv_prop.collection? then
162
+ lambda { |other| add_to_inverse_collection(other, accessors, inv_rdr) }
163
+ else
164
+ lambda { |other| set_inversible_noncollection_attribute(other, accessors, inv_wtr) }
165
+ end
166
+ end
167
+ logger.debug { "Injected inverse #{inverse} updater into #{qp}.#{attribute} writer method #{wtr}." }
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,169 @@
1
+ require 'jinx/helpers/inflector'
2
+ require 'jinx/metadata/property'
3
+
4
+ module Jinx
5
+ # The attribute metadata for an introspected Java property.
6
+ class JavaProperty < Property
7
+
8
+ # This property's Java property descriptor.
9
+ attr_reader :property_descriptor
10
+
11
+ # This property's Java property [reader, writer] accessors, e.g. +[:getActivityStatus, :setActivityStatus]+.
12
+ attr_reader :property_accessors
13
+
14
+ # Creates a Ruby Property symbol corresponding to the given Ruby Java class wrapper klazz
15
+ # and Java property_descriptor.
16
+ #
17
+ # The property name is the lower-case, underscore property descriptor name with the alterations
18
+ # described in {JavaProperty.to_attribute_symbol} and {Class#unocclude_reserved_method}.
19
+ #
20
+ # The property type is inferred as follows:
21
+ # * If the property descriptor return type is a primitive Java type, then that type is returned.
22
+ # * If the return type is a parameterized collection, then the parameter type is returned.
23
+ # * If the return type is an unparameterized collection, then this method infers the type from
24
+ # the property name, e.g. +StudyProtocolCollection+type is inferred as +StudyProtocol+
25
+ # by stripping the +Collection+ suffix, capitalizing the prefix and looking for a class of
26
+ # that name in the {Metadata#domain_module}.
27
+ # * Otherwise, this method returns Java::Javalang::Object.
28
+ #
29
+ # The optional restricted_type argument restricts the property to a subclass of the declared
30
+ # property type.
31
+ def initialize(pd, declarer, restricted_type=nil)
32
+ symbol = create_standard_attribute_symbol(pd, declarer)
33
+ super(symbol, declarer, restricted_type)
34
+ @property_descriptor = pd
35
+ # deficient Java introspector does not recognize 'is' prefix for a Boolean property
36
+ rm = declarer.property_read_method(pd)
37
+ Jinx.fail(ArgumentError, "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
42
+ Jinx.fail(ArgumentError, "Reader method not found for #{declarer} property #{pd.name}")
43
+ end
44
+ end
45
+ unless pd.write_method then
46
+ Jinx.fail(ArgumentError, "Property does not have a write method: #{declarer.qp}.#{pd.name}")
47
+ end
48
+ writer = pd.write_method.name.to_sym
49
+ unless declarer.method_defined?(writer) then
50
+ Jinx.fail(ArgumentError, "Writer method not found for #{declarer} property #{pd.name}")
51
+ end
52
+ @property_accessors = [reader, writer]
53
+ qualify(:collection) if collection_java_class?
54
+ @type = infer_type
55
+ end
56
+
57
+ # @return [Symbol] the JRuby wrapper method for the Java property reader
58
+ def property_reader
59
+ property_accessors.first
60
+ end
61
+
62
+ # @return [Symbol] the JRuby wrapper method for the Java property writer
63
+ def property_writer
64
+ property_accessors.last
65
+ end
66
+
67
+ # Returns a lower-case, underscore symbol for the given property_name.
68
+ # A name ending in 'Collection' is changed to a pluralization.
69
+ #
70
+ # @example
71
+ # JavaProperty.to_attribute_symbol('specimenEventCollection') #=> :specimen_events
72
+ def self.to_attribute_symbol(property_name)
73
+ name = if property_name =~ /(.+)Collection$/ then
74
+ property_name[0...-'Collection'.length].pluralize.underscore
75
+ else
76
+ property_name.underscore
77
+ end
78
+ name.to_sym
79
+ end
80
+
81
+ private
82
+
83
+ # @param pd the Java property descriptor
84
+ # @param [Class] klass the declarer
85
+ # @return [String] the lower-case, underscore symbol for the given property descriptor
86
+ def create_standard_attribute_symbol(pd, klass)
87
+ propname = pd.name
88
+ name = propname.underscore
89
+ renamed = klass.unocclude_reserved_method(pd)
90
+ if renamed then
91
+ logger.debug { "Renamed #{klass.qp} reserved Ruby method #{name} to #{renamed}." }
92
+ renamed
93
+ else
94
+ JavaProperty.to_attribute_symbol(propname)
95
+ end
96
+ end
97
+
98
+ # @return [Boolean] whether this property's Java type is +Iterable+
99
+ 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
+ end
105
+
106
+ # @return [Class] the type for the specified klass property descriptor pd as described in {#initialize}
107
+ def infer_type
108
+ collection? ? infer_collection_type : infer_non_collection_type
109
+ end
110
+
111
+ # Returns the domain type for this property's Java Collection property descriptor.
112
+ # If the property type is parameterized by a single domain class, then that generic type argument is the domain type.
113
+ # Otherwise, the type is inferred from the property name as described in {#infer_collection_type_from_name}.
114
+ #
115
+ # @return [Class] this property's Ruby type
116
+ def infer_collection_type
117
+ generic_parameter_type or infer_collection_type_from_name or Java::JavaLang::Object
118
+ end
119
+
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
+ # @return [Class, nil] the domain type of this property's property descriptor Collection generic
127
+ # type argument, or nil if none
128
+ def generic_parameter_type
129
+ method = @property_descriptor.readMethod || return
130
+ gtype = method.genericReturnType
131
+ return unless Java::JavaLangReflect::ParameterizedType === gtype
132
+ atypes = gtype.actualTypeArguments
133
+ return unless atypes.size == 1
134
+ atype = atypes[0]
135
+ klass = java_to_ruby_class(atype)
136
+ logger.debug { "Inferred #{declarer.qp} #{self} domain type #{klass.qp} from generic parameter #{atype.name}." } if klass
137
+ klass
138
+ end
139
+
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
+ # Returns the domain type for this property's collection Java property descriptor name.
148
+ # By convention, Jinx domain collection propertys often begin with a domain type
149
+ # name and end in 'Collection'. This method strips the Collection suffix and checks
150
+ # whether the prefix is a domain class.
151
+ #
152
+ # For example, the type of the property named +distributionProtocolCollection+
153
+ # is inferred as +DistributionProtocol+ by stripping the +Collection+ suffix,
154
+ # capitalizing the prefix and looking for a class of that name in this classifier's
155
+ # domain_module.
156
+ #
157
+ # @return [Class] the collection item type
158
+ def infer_collection_type_from_name
159
+ # the property name
160
+ pname = @property_descriptor.name
161
+ # The potential class name is the capitalized property name without a 'Collection' suffix.
162
+ cname = pname.capitalize_first.sub(/Collection$/, '')
163
+ jname = [@declarer.parent_module, cname].join('::')
164
+ klass = eval jname rescue nil
165
+ if klass then logger.debug { "Inferred #{declarer.qp} #{self} collection domain type #{klass.qp} from the property name." } end
166
+ klass
167
+ end
168
+ end
169
+ end