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