jinx 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +27 -0
- data/History.md +6 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/Rakefile +41 -0
- data/examples/family/README.md +10 -0
- data/examples/family/ext/build.xml +35 -0
- data/examples/family/ext/src/family/Address.java +68 -0
- data/examples/family/ext/src/family/Child.java +24 -0
- data/examples/family/ext/src/family/DomainObject.java +26 -0
- data/examples/family/ext/src/family/Household.java +36 -0
- data/examples/family/ext/src/family/Parent.java +48 -0
- data/examples/family/ext/src/family/Person.java +42 -0
- data/examples/family/lib/family.rb +15 -0
- data/examples/family/lib/family/address.rb +6 -0
- data/examples/family/lib/family/domain_object.rb +6 -0
- data/examples/family/lib/family/household.rb +6 -0
- data/examples/family/lib/family/parent.rb +16 -0
- data/examples/family/lib/family/person.rb +6 -0
- data/examples/model/README.md +25 -0
- data/examples/model/ext/build.xml +35 -0
- data/examples/model/ext/src/domain/Child.java +192 -0
- data/examples/model/ext/src/domain/Dependent.java +29 -0
- data/examples/model/ext/src/domain/DomainObject.java +26 -0
- data/examples/model/ext/src/domain/Independent.java +83 -0
- data/examples/model/ext/src/domain/Parent.java +129 -0
- data/examples/model/ext/src/domain/Person.java +14 -0
- data/examples/model/lib/model.rb +13 -0
- data/examples/model/lib/model/child.rb +13 -0
- data/examples/model/lib/model/domain_object.rb +6 -0
- data/examples/model/lib/model/independent.rb +11 -0
- data/examples/model/lib/model/parent.rb +17 -0
- data/jinx.gemspec +22 -0
- data/lib/jinx.rb +3 -0
- data/lib/jinx/active_support/README.txt +2 -0
- data/lib/jinx/active_support/core_ext/string.rb +7 -0
- data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
- data/lib/jinx/active_support/inflections.rb +55 -0
- data/lib/jinx/active_support/inflector.rb +398 -0
- data/lib/jinx/cli/application.rb +36 -0
- data/lib/jinx/cli/command.rb +214 -0
- data/lib/jinx/helpers/array.rb +108 -0
- data/lib/jinx/helpers/boolean.rb +42 -0
- data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
- data/lib/jinx/helpers/class.rb +149 -0
- data/lib/jinx/helpers/collection.rb +33 -0
- data/lib/jinx/helpers/collections.rb +11 -0
- data/lib/jinx/helpers/collector.rb +20 -0
- data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
- data/lib/jinx/helpers/enumerable.rb +242 -0
- data/lib/jinx/helpers/enumerate.rb +35 -0
- data/lib/jinx/helpers/error.rb +15 -0
- data/lib/jinx/helpers/file_separator.rb +65 -0
- data/lib/jinx/helpers/filter.rb +52 -0
- data/lib/jinx/helpers/flattener.rb +38 -0
- data/lib/jinx/helpers/hash.rb +12 -0
- data/lib/jinx/helpers/hashable.rb +502 -0
- data/lib/jinx/helpers/inflector.rb +36 -0
- data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
- data/lib/jinx/helpers/lazy_hash.rb +44 -0
- data/lib/jinx/helpers/log.rb +106 -0
- data/lib/jinx/helpers/math.rb +12 -0
- data/lib/jinx/helpers/merge.rb +60 -0
- data/lib/jinx/helpers/module.rb +18 -0
- data/lib/jinx/helpers/multi_enumerator.rb +31 -0
- data/lib/jinx/helpers/options.rb +92 -0
- data/lib/jinx/helpers/os.rb +19 -0
- data/lib/jinx/helpers/partial_order.rb +37 -0
- data/lib/jinx/helpers/pretty_print.rb +207 -0
- data/lib/jinx/helpers/set.rb +8 -0
- data/lib/jinx/helpers/stopwatch.rb +76 -0
- data/lib/jinx/helpers/transformer.rb +24 -0
- data/lib/jinx/helpers/transitive_closure.rb +55 -0
- data/lib/jinx/helpers/uniquifier.rb +50 -0
- data/lib/jinx/helpers/validation.rb +33 -0
- data/lib/jinx/helpers/visitor.rb +370 -0
- data/lib/jinx/import/class_path_modifier.rb +77 -0
- data/lib/jinx/import/java.rb +337 -0
- data/lib/jinx/importer.rb +240 -0
- data/lib/jinx/metadata.rb +155 -0
- data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
- data/lib/jinx/metadata/dependency.rb +244 -0
- data/lib/jinx/metadata/id_alias.rb +23 -0
- data/lib/jinx/metadata/introspector.rb +179 -0
- data/lib/jinx/metadata/inverse.rb +170 -0
- data/lib/jinx/metadata/java_property.rb +169 -0
- data/lib/jinx/metadata/propertied.rb +500 -0
- data/lib/jinx/metadata/property.rb +401 -0
- data/lib/jinx/metadata/property_characteristics.rb +114 -0
- data/lib/jinx/resource.rb +862 -0
- data/lib/jinx/resource/copy_visitor.rb +36 -0
- data/lib/jinx/resource/inversible.rb +90 -0
- data/lib/jinx/resource/match_visitor.rb +180 -0
- data/lib/jinx/resource/matcher.rb +20 -0
- data/lib/jinx/resource/merge_visitor.rb +73 -0
- data/lib/jinx/resource/mergeable.rb +185 -0
- data/lib/jinx/resource/reference_enumerator.rb +49 -0
- data/lib/jinx/resource/reference_path_visitor.rb +38 -0
- data/lib/jinx/resource/reference_visitor.rb +55 -0
- data/lib/jinx/resource/unique.rb +35 -0
- data/lib/jinx/version.rb +3 -0
- data/spec/defaults_spec.rb +30 -0
- data/spec/definitions/model/alias/child.rb +5 -0
- data/spec/definitions/model/base/child.rb +5 -0
- data/spec/definitions/model/base/domain_object.rb +5 -0
- data/spec/definitions/model/base/independent.rb +5 -0
- data/spec/definitions/model/defaults/child.rb +5 -0
- data/spec/definitions/model/dependency/child.rb +5 -0
- data/spec/definitions/model/dependency/parent.rb +6 -0
- data/spec/definitions/model/inverse/child.rb +5 -0
- data/spec/definitions/model/inverse/independent.rb +5 -0
- data/spec/definitions/model/inverse/parent.rb +5 -0
- data/spec/definitions/model/mandatory/child.rb +6 -0
- data/spec/dependency_spec.rb +47 -0
- data/spec/family_spec.rb +64 -0
- data/spec/inverse_spec.rb +53 -0
- data/spec/mandatory_spec.rb +43 -0
- data/spec/metadata_spec.rb +68 -0
- data/spec/resource_spec.rb +30 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/model.rb +19 -0
- data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
- data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
- data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
- data/test/fixtures/mixed/ext/build.xml +35 -0
- data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
- data/test/helper.rb +7 -0
- data/test/lib/jinx/command_test.rb +41 -0
- data/test/lib/jinx/helpers/boolean_test.rb +27 -0
- data/test/lib/jinx/helpers/class_test.rb +60 -0
- data/test/lib/jinx/helpers/collections_test.rb +402 -0
- data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
- data/test/lib/jinx/helpers/inflector_test.rb +11 -0
- data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
- data/test/lib/jinx/helpers/module_test.rb +24 -0
- data/test/lib/jinx/helpers/options_test.rb +66 -0
- data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
- data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
- data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
- data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
- data/test/lib/jinx/helpers/visitor_test.rb +288 -0
- data/test/lib/jinx/import/java_test.rb +78 -0
- data/test/lib/jinx/import/mixed_case_test.rb +16 -0
- 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
|