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,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