caruby-core 1.4.9 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.md +48 -0
- data/lib/caruby/cli/command.rb +2 -1
- data/lib/caruby/csv/csv_mapper.rb +8 -8
- data/lib/caruby/database/persistable.rb +44 -65
- data/lib/caruby/database/persistence_service.rb +12 -9
- data/lib/caruby/database/persistifier.rb +14 -14
- data/lib/caruby/database/reader.rb +53 -51
- data/lib/caruby/database/search_template_builder.rb +9 -10
- data/lib/caruby/database/store_template_builder.rb +58 -58
- data/lib/caruby/database/writer.rb +96 -96
- data/lib/caruby/database.rb +19 -19
- data/lib/caruby/domain/attribute.rb +581 -0
- data/lib/caruby/domain/attributes.rb +615 -0
- data/lib/caruby/domain/dependency.rb +240 -0
- data/lib/caruby/domain/importer.rb +183 -0
- data/lib/caruby/domain/introspection.rb +176 -0
- data/lib/caruby/domain/inverse.rb +173 -0
- data/lib/caruby/domain/inversible.rb +1 -2
- data/lib/caruby/domain/java_attribute.rb +173 -0
- data/lib/caruby/domain/merge.rb +13 -10
- data/lib/caruby/domain/metadata.rb +141 -0
- data/lib/caruby/domain/mixin.rb +35 -0
- data/lib/caruby/domain/reference_visitor.rb +5 -3
- data/lib/caruby/domain.rb +340 -0
- data/lib/caruby/import/java.rb +29 -25
- data/lib/caruby/migration/migratable.rb +5 -5
- data/lib/caruby/migration/migrator.rb +19 -15
- data/lib/caruby/migration/resource_module.rb +1 -1
- data/lib/caruby/resource.rb +39 -30
- data/lib/caruby/util/collection.rb +94 -33
- data/lib/caruby/util/coordinate.rb +28 -2
- data/lib/caruby/util/log.rb +4 -4
- data/lib/caruby/util/module.rb +12 -28
- data/lib/caruby/util/partial_order.rb +9 -10
- data/lib/caruby/util/pretty_print.rb +46 -26
- data/lib/caruby/util/topological_sync_enumerator.rb +10 -4
- data/lib/caruby/util/transitive_closure.rb +2 -2
- data/lib/caruby/util/visitor.rb +1 -1
- data/lib/caruby/version.rb +1 -1
- data/test/lib/caruby/database/persistable_test.rb +1 -1
- data/test/lib/caruby/domain/domain_test.rb +14 -28
- data/test/lib/caruby/domain/inversible_test.rb +1 -1
- data/test/lib/caruby/import/java_test.rb +5 -0
- data/test/lib/caruby/migration/test_case.rb +0 -1
- data/test/lib/caruby/test_case.rb +9 -10
- data/test/lib/caruby/util/collection_test.rb +23 -5
- data/test/lib/caruby/util/module_test.rb +10 -14
- data/test/lib/caruby/util/partial_order_test.rb +16 -15
- data/test/lib/caruby/util/visitor_test.rb +1 -1
- data/test/lib/examples/galena/clinical_trials/migration/test_case.rb +1 -1
- metadata +16 -15
- data/History.txt +0 -44
- data/lib/caruby/domain/attribute_metadata.rb +0 -551
- data/lib/caruby/domain/java_attribute_metadata.rb +0 -183
- data/lib/caruby/domain/resource_attributes.rb +0 -565
- data/lib/caruby/domain/resource_dependency.rb +0 -217
- data/lib/caruby/domain/resource_introspection.rb +0 -160
- data/lib/caruby/domain/resource_inverse.rb +0 -151
- data/lib/caruby/domain/resource_metadata.rb +0 -155
- data/lib/caruby/domain/resource_module.rb +0 -370
- data/lib/caruby/yard/resource_metadata_handler.rb +0 -8
@@ -1,217 +0,0 @@
|
|
1
|
-
require 'caruby/util/validation'
|
2
|
-
|
3
|
-
module CaRuby
|
4
|
-
# ResourceMetadata mix-in to capture Resource dependency.
|
5
|
-
module ResourceDependency
|
6
|
-
|
7
|
-
attr_reader :owners, :owner_attributes
|
8
|
-
|
9
|
-
# Returns the attribute which references the dependent type, or nil if none.
|
10
|
-
def dependent_attribute(type)
|
11
|
-
dependent_attributes.detect { |attr| type <= domain_type(attr) }
|
12
|
-
end
|
13
|
-
|
14
|
-
# Adds the given attribute as a dependent.
|
15
|
-
#
|
16
|
-
# Supported flags include the following:
|
17
|
-
# * :logical - the dependency relation is not cascaded by the application
|
18
|
-
# * :autogenerated - a dependent can be created by the application as a side-effect of creating the owner
|
19
|
-
# * :disjoint - the dependent owner has more than one owner attribute, but only one owner instance
|
20
|
-
#
|
21
|
-
# If the attribute inverse is not a collection, then the attribute writer
|
22
|
-
# is modified to delegate to the dependent owner writer. This enforces
|
23
|
-
# referential integrity by ensuring that the following post-condition holds:
|
24
|
-
# * _owner_._attribute_._inverse_ == _owner_
|
25
|
-
# where:
|
26
|
-
# * _owner_ is an instance this attribute's declaring class
|
27
|
-
# * _inverse_ is the owner inverse attribute defined in the dependent class
|
28
|
-
#
|
29
|
-
# @param [Symbol] attribute the dependent to add
|
30
|
-
# @param [<Symbol>] flags the attribute qualifier flags
|
31
|
-
def add_dependent_attribute(attribute, *flags)
|
32
|
-
attr_md = attribute_metadata(attribute)
|
33
|
-
flags << :dependent unless flags.include?(:dependent)
|
34
|
-
attr_md.qualify(*flags)
|
35
|
-
inverse = attr_md.inverse
|
36
|
-
inv_type = attr_md.type
|
37
|
-
# example: Parent.add_dependent_attribute(:children) with inverse :parent calls the following:
|
38
|
-
# Child.add_owner(Parent, :children, :parent)
|
39
|
-
inv_type.add_owner(self, attribute, inverse)
|
40
|
-
end
|
41
|
-
|
42
|
-
# Makes a new owner attribute. The attribute name is the lower-case demodulized
|
43
|
-
# owner class name. The owner class must reference this class via the given
|
44
|
-
# inverse dependent attribute.
|
45
|
-
#
|
46
|
-
# @param klass (see #detect_owner_attribute)
|
47
|
-
# @param [Symbol] the owner -> dependent inverse attribute
|
48
|
-
# @return [Symbol] this class's new owner attribute
|
49
|
-
# @raise [ArgumentError] if the inverse is nil
|
50
|
-
def create_owner_attribute(klass, inverse)
|
51
|
-
if inverse.nil? then
|
52
|
-
raise ArgumentError.new("Cannot create a #{qp} owner attribute to #{klass} without a dependent attribute to this class.")
|
53
|
-
end
|
54
|
-
attr = klass.name.demodulize.underscore.to_sym
|
55
|
-
attr_accessor(attr)
|
56
|
-
add_attribute(attr, klass)
|
57
|
-
attribute_metadata(attr).inverse = inverse
|
58
|
-
logger.debug { "Created #{qp} owner attribute #{attr} with inverse #{klass.qp}.#{inverse}." }
|
59
|
-
attr
|
60
|
-
end
|
61
|
-
|
62
|
-
# @return [Boolean] whether this class depends on an owner
|
63
|
-
def dependent?
|
64
|
-
not owners.empty?
|
65
|
-
end
|
66
|
-
|
67
|
-
# @return [Boolean] whether this class has an owner which cascades save operations to this dependent
|
68
|
-
def cascaded_dependent?
|
69
|
-
owner_attribute_metadata_enumerator.any? { |attr_md| attr_md.cascaded? }
|
70
|
-
end
|
71
|
-
|
72
|
-
# @return [Boolean] whether this class depends the given other class
|
73
|
-
def depends_on?(other)
|
74
|
-
owners.detect { |owner| owner === other }
|
75
|
-
end
|
76
|
-
|
77
|
-
# @return [Symbol, nil] the attribute which references the dependent type,
|
78
|
-
# or nil if none
|
79
|
-
def dependent_attribute(dep_type)
|
80
|
-
type = dependent_attributes.detect { |attr| domain_type(attr) == dep_type }
|
81
|
-
return type if type
|
82
|
-
dependent_attribute(dep_type.superclass) if dep_type.superclass < Resource
|
83
|
-
end
|
84
|
-
|
85
|
-
# @return [Symbol, nil] the sole owner attribute of this class, or nil if there
|
86
|
-
# is not exactly one owner
|
87
|
-
def owner_attribute
|
88
|
-
attr_md = owner_attribute_metadata_enumerator.first || return
|
89
|
-
# There is at most one non-nil value in the owner class => AttributeMetadata hash.
|
90
|
-
# If there is such a value, then return the attribute symbol.
|
91
|
-
if owner_attribute_metadata_enumerator.size == 1 then
|
92
|
-
attr_md.to_sym
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# @return [<Symbol>] this class's owner attributes
|
97
|
-
def owner_attributes
|
98
|
-
owner_attribute_metadata_enumerator.transform { |attr_md| attr_md.to_sym }
|
99
|
-
end
|
100
|
-
|
101
|
-
# @return [<Class>] this class's dependent types
|
102
|
-
def dependents
|
103
|
-
dependent_attributes.wrap { |attr| attr.type }
|
104
|
-
end
|
105
|
-
|
106
|
-
# @return [<Class>] this class's owner types
|
107
|
-
def owners
|
108
|
-
@owners ||= Enumerable::Enumerator.new(owner_attribute_metadata_hash, :each_key)
|
109
|
-
end
|
110
|
-
|
111
|
-
protected
|
112
|
-
|
113
|
-
# Adds the given owner class to this dependent class.
|
114
|
-
# This method must be called before any dependent attribute is accessed.
|
115
|
-
# If the attribute is given, then the attribute inverse is set.
|
116
|
-
# Otherwise, if there is not already an owner attribute, then a new owner attribute is created.
|
117
|
-
# The name of the new attribute is the lower-case demodulized owner class name.
|
118
|
-
#
|
119
|
-
# @param [Class] the owner class
|
120
|
-
# @param [Symbol] inverse the owner -> dependent attribute
|
121
|
-
# @param [Symbol, nil] attribute the dependent -> owner attribute, if known
|
122
|
-
# @raise [ValidationError] if the inverse is nil
|
123
|
-
def add_owner(klass, inverse, attribute=nil)
|
124
|
-
if inverse.nil? then raise ValidationError.new("Owner #{klass.qp} missing dependent attribute for dependent #{qp}") end
|
125
|
-
logger.debug { "Adding #{qp} owner #{klass.qp}#{' attribute ' + attribute.to_s if attribute}#{' inverse ' + inverse.to_s if inverse}..." }
|
126
|
-
if @owner_attr_hash then
|
127
|
-
raise MetadataError.new("Can't add #{qp} owner #{klass.qp} after dependencies have been accessed")
|
128
|
-
end
|
129
|
-
|
130
|
-
# detect the owner attribute, if necessary
|
131
|
-
attribute ||= detect_owner_attribute(klass, inverse)
|
132
|
-
attr_md = attribute_metadata(attribute) if attribute
|
133
|
-
# Add the owner class => attribute entry.
|
134
|
-
# The attribute is nil if the dependency is unidirectional, i.e. there is an owner class which
|
135
|
-
# references this class via a dependency attribute but there is no inverse owner attribute.
|
136
|
-
local_owner_attribute_metadata_hash[klass] = attr_md
|
137
|
-
# If the dependency is unidirectional, then our job is done.
|
138
|
-
return if attribute.nil?
|
139
|
-
|
140
|
-
# set the inverse if necessary
|
141
|
-
unless attr_md.inverse then
|
142
|
-
set_attribute_inverse(attribute, inverse)
|
143
|
-
end
|
144
|
-
# set the owner flag if necessary
|
145
|
-
unless attr_md.owner? then attr_md.qualify(:owner) end
|
146
|
-
# Redefine the writer method to warn when changing the owner
|
147
|
-
rdr, wtr = attr_md.accessors
|
148
|
-
logger.debug { "Injecting owner change warning into #{qp}.#{attribute} writer method #{wtr}..." }
|
149
|
-
redefine_method(wtr) do |old_wtr|
|
150
|
-
lambda do |ref|
|
151
|
-
prev = send(rdr)
|
152
|
-
if prev and prev != ref then
|
153
|
-
if ref.nil? then
|
154
|
-
logger.warn("Unsetting the #{self} owner #{attribute} #{prev}.")
|
155
|
-
elsif ref.identifier != prev.identifier then
|
156
|
-
logger.warn("Resetting the #{self} owner #{attribute} from #{prev} to #{ref}.")
|
157
|
-
end
|
158
|
-
end
|
159
|
-
send(old_wtr, ref)
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
# Adds the given attribute as an owner. This method is called when a new attribute is added that
|
165
|
-
# references an existing owner.
|
166
|
-
#
|
167
|
-
# @param [Symbol] attribute the owner attribute
|
168
|
-
def add_owner_attribute(attribute)
|
169
|
-
attr_md = attribute_metadata(attribute)
|
170
|
-
otype = attr_md.type
|
171
|
-
hash = local_owner_attribute_metadata_hash
|
172
|
-
if hash.include?(otype) then
|
173
|
-
oattr = hash[otype]
|
174
|
-
unless oattr.nil? then
|
175
|
-
raise MetadataError.new("Cannot set #{qp} owner attribute to #{attribute} since it is already set to #{oattr}")
|
176
|
-
end
|
177
|
-
hash[otype] = attr_md
|
178
|
-
else
|
179
|
-
add_owner(otype, attr_md.inverse, attribute)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
# @return [{Class => AttributeMetadata}] this class's owner type => attribute hash
|
184
|
-
def owner_attribute_metadata_hash
|
185
|
-
@oa_hash ||= create_owner_attribute_metadata_hash
|
186
|
-
end
|
187
|
-
|
188
|
-
private
|
189
|
-
|
190
|
-
def local_owner_attribute_metadata_hash
|
191
|
-
@local_oa_hash ||= {}
|
192
|
-
end
|
193
|
-
|
194
|
-
# @return [{Class => AttributeMetadata}] a new owner type => attribute hash
|
195
|
-
def create_owner_attribute_metadata_hash
|
196
|
-
local = local_owner_attribute_metadata_hash
|
197
|
-
superclass < Resource ? local.union(superclass.owner_attribute_metadata_hash) : local
|
198
|
-
end
|
199
|
-
|
200
|
-
# @return [<AttributeMetadata>] the owner attributes
|
201
|
-
def owner_attribute_metadata_enumerator
|
202
|
-
# Enumerate each owner AttributeMetadata, filtering out nil values.
|
203
|
-
@oa_enum ||= Enumerable::Enumerator.new(owner_attribute_metadata_hash, :each_value).filter
|
204
|
-
end
|
205
|
-
|
206
|
-
# Returns the attribute which references the owner. The owner attribute is the inverse
|
207
|
-
# of the given owner class inverse attribute, if it exists. Otherwise, the owner
|
208
|
-
# attribute is inferred by #{ResourceInverse#detect_inverse_attribute}.
|
209
|
-
|
210
|
-
# @param klass (see #add_owner)
|
211
|
-
# @param [Symbol] inverse the owner -> dependent attribute
|
212
|
-
# @return [Symbol, nil] this class's owner attribute
|
213
|
-
def detect_owner_attribute(klass, inverse)
|
214
|
-
klass.attribute_metadata(inverse).inverse or detect_inverse_attribute(klass)
|
215
|
-
end
|
216
|
-
end
|
217
|
-
end
|
@@ -1,160 +0,0 @@
|
|
1
|
-
require 'caruby/import/java'
|
2
|
-
require 'caruby/domain/java_attribute_metadata'
|
3
|
-
|
4
|
-
module CaRuby
|
5
|
-
# ResourceMetadata mix-in to infer attribute meta-data from Java properties.
|
6
|
-
module ResourceIntrospection
|
7
|
-
|
8
|
-
private
|
9
|
-
|
10
|
-
# Defines the Java property attribute and standard attribute methods, e.g.
|
11
|
-
# +study_protocol+ and +studyProtocol+. A boolean attribute is provisioned
|
12
|
-
# with an additional reader alias, e.g. +available?+ for +is_available+.
|
13
|
-
#
|
14
|
-
# Each Java property attribute delegates to the Java property getter and setter.
|
15
|
-
# Each standard attribute delegates to the Java property attribute.
|
16
|
-
# Redefining these methods results in a call to the redefined method.
|
17
|
-
# This contrasts with a Ruby alias, where the alias remains bound to the original method body.
|
18
|
-
def introspect
|
19
|
-
init_attributes # in ResourceAttributes
|
20
|
-
logger.debug { "Introspecting #{qp} metadata..." }
|
21
|
-
# filter properties for those with both a read and write method
|
22
|
-
pds = java_properties(false)
|
23
|
-
# define the standard Java attribute methods
|
24
|
-
pds.each { |pd| define_java_attribute(pd) }
|
25
|
-
logger.debug { "Introspection of #{qp} metadata complete." }
|
26
|
-
self
|
27
|
-
end
|
28
|
-
|
29
|
-
# Defines the Java property attribute and standard attribute methods, e.g.
|
30
|
-
# +study_protocol+ and +studyProtocol+. A boolean attribute is provisioned
|
31
|
-
# with an additional reader alias, e.g. +available?+ for +is_available+.
|
32
|
-
#
|
33
|
-
# A standard attribute which differs from the property attribute delegates
|
34
|
-
# to the property attribute, e.g. +study_protocol+ delegates to +studyProtocol+
|
35
|
-
# rather than aliasing +setStudyProtocol+. Redefining these methods results
|
36
|
-
# in a call to the redefined method. This contrasts with a Ruby alias,
|
37
|
-
# where each attribute alias is bound to the respective property reader or
|
38
|
-
# writer.
|
39
|
-
def define_java_attribute(pd)
|
40
|
-
if transient?(pd) then
|
41
|
-
logger.debug { "Ignoring #{name.demodulize} transient property #{pd.name}." }
|
42
|
-
return
|
43
|
-
end
|
44
|
-
# the standard underscore lower-case attributes
|
45
|
-
attr = create_java_attribute(pd)
|
46
|
-
# delegate the standard attribute accessors to the property accessors
|
47
|
-
alias_attribute_property(attr, pd.name)
|
48
|
-
# add special wrappers
|
49
|
-
wrap_java_attribute(attr, pd)
|
50
|
-
# create Ruby alias for boolean, e.g. alias :empty? for :empty
|
51
|
-
if pd.property_type.name[/\w+$/].downcase == 'boolean' then
|
52
|
-
# strip leading is_, if any, before appending question mark
|
53
|
-
aliaz = attr.to_s[/^(is_)?(\w+)/, 2] << '?'
|
54
|
-
delegate_to_attribute(aliaz, attr)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
# Adds a filter to the attribute access method for the property descriptor pd if it is a String or Date.
|
59
|
-
def wrap_java_attribute(attribute, pd)
|
60
|
-
if pd.propertyType == Java::JavaLang::String.java_class then
|
61
|
-
wrap_java_string_attribute(attribute, pd)
|
62
|
-
elsif pd.propertyType == Java::JavaUtil::Date.java_class then
|
63
|
-
wrap_java_date_attribute(attribute, pd)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Adds a to_s filter to this Class's String property access methods.
|
68
|
-
def wrap_java_string_attribute(attribute, pd)
|
69
|
-
# filter the attribute writer
|
70
|
-
awtr = "#{attribute}=".to_sym
|
71
|
-
pwtr = pd.write_method.name.to_sym
|
72
|
-
define_method(awtr) do |value|
|
73
|
-
stdval = value.to_s unless value.nil_or_empty?
|
74
|
-
send(pwtr, stdval)
|
75
|
-
end
|
76
|
-
logger.debug { "Filtered #{qp} #{awtr} method with non-String -> String converter." }
|
77
|
-
end
|
78
|
-
|
79
|
-
# Adds a date parser filter to this Class's Date property access methods.
|
80
|
-
def wrap_java_date_attribute(attribute, pd)
|
81
|
-
# filter the attribute reader
|
82
|
-
prdr = pd.read_method.name.to_sym
|
83
|
-
define_method(attribute) do
|
84
|
-
value = send(prdr)
|
85
|
-
Java::JavaUtil::Date === value ? value.to_ruby_date : value
|
86
|
-
end
|
87
|
-
|
88
|
-
# filter the attribute writer
|
89
|
-
awtr = "#{attribute}=".to_sym
|
90
|
-
pwtr = pd.write_method.name.to_sym
|
91
|
-
define_method(awtr) do |value|
|
92
|
-
value = Java::JavaUtil::Date.from_ruby_date(value) if ::Date === value
|
93
|
-
send(pwtr, value)
|
94
|
-
end
|
95
|
-
|
96
|
-
logger.debug { "Filtered #{qp} #{attribute} and #{awtr} methods with Java Date <-> Ruby Date converter." }
|
97
|
-
end
|
98
|
-
|
99
|
-
# Aliases the methods _aliaz_ and _aliaz=_ to _property_ and _property=_, resp.,
|
100
|
-
# where _property_ is the Java property name for the attribute.
|
101
|
-
def alias_attribute_property(aliaz, attribute)
|
102
|
-
# strip the Java reader and writer is/get/set prefix and make a symbol
|
103
|
-
prdr, pwtr = attribute_metadata(attribute).property_accessors
|
104
|
-
alias_method(aliaz, prdr)
|
105
|
-
writer = "#{aliaz}=".to_sym
|
106
|
-
alias_method(writer, pwtr)
|
107
|
-
end
|
108
|
-
|
109
|
-
# Makes a standard attribute for the given property descriptor.
|
110
|
-
# Adds a camelized Java-like alias to the standard attribute.
|
111
|
-
#
|
112
|
-
# caTissue alert - DE annotation collection attributes are often misnamed,
|
113
|
-
# e.g. +histologic_grade+ for a +HistologicGrade+ collection attribute.
|
114
|
-
# This is fixed by adding a pluralized alias, e.g. +histologic_grades+.
|
115
|
-
#
|
116
|
-
# @return a new attribute symbol created for the given PropertyDescriptor pd
|
117
|
-
def create_java_attribute(pd)
|
118
|
-
# make the attribute metadata
|
119
|
-
attr_md = JavaAttributeMetadata.new(pd, self)
|
120
|
-
add_attribute_metadata(attr_md)
|
121
|
-
# the property name is an alias for the standard attribute
|
122
|
-
std_attr = attr_md.to_sym
|
123
|
-
prop_attr = pd.name.to_sym
|
124
|
-
delegate_to_attribute(prop_attr, std_attr) unless prop_attr == std_attr
|
125
|
-
|
126
|
-
# alias a misnamed collection attribute, if necessary
|
127
|
-
if attr_md.collection? then
|
128
|
-
name = std_attr.to_s
|
129
|
-
if name.singularize == name then
|
130
|
-
aliaz = name.pluralize.to_sym
|
131
|
-
if aliaz != name then
|
132
|
-
logger.debug { "Adding annotation #{qp} alias #{aliaz} to the misnamed collection attribute #{std_attr}..." }
|
133
|
-
delegate_to_attribute(aliaz, std_attr)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
std_attr
|
139
|
-
end
|
140
|
-
|
141
|
-
# Defines methods _aliaz_ and _aliaz=_ which calls the standard _attribute_ and
|
142
|
-
# _attribute=_ accessor methods, resp.
|
143
|
-
# Calling rather than aliasing the attribute accessor allows the aliaz accessor to
|
144
|
-
# reflect a change to the attribute accessor.
|
145
|
-
def delegate_to_attribute(aliaz, attribute)
|
146
|
-
rdr, wtr = attribute_metadata(attribute).accessors
|
147
|
-
define_method(aliaz) { send(rdr) }
|
148
|
-
define_method("#{aliaz}=".to_sym) { |value| send(wtr, value) }
|
149
|
-
add_alias(aliaz, attribute)
|
150
|
-
end
|
151
|
-
|
152
|
-
# Makes a new synthetic attribute for each _method_ => _original_ hash entry.
|
153
|
-
#
|
154
|
-
# @param (see Class#offset_attr_accessor)
|
155
|
-
def offset_attribute(hash, offset=nil)
|
156
|
-
offset_attr_accessor(hash, offset)
|
157
|
-
hash.each { |attr, original| add_attribute(attr, attribute_metadata(original).type) }
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
@@ -1,151 +0,0 @@
|
|
1
|
-
require 'caruby/import/java'
|
2
|
-
require 'caruby/domain/java_attribute_metadata'
|
3
|
-
|
4
|
-
module CaRuby
|
5
|
-
# ResourceMetadata mix-in to infer and set inverse attributes.
|
6
|
-
module ResourceInverse
|
7
|
-
|
8
|
-
protected
|
9
|
-
|
10
|
-
# Infers the inverse of the given attribute declared by this class. A domain attribute is
|
11
|
-
# recognized as an inverse according to the {ResourceInverse#detect_inverse_attribute}
|
12
|
-
# criterion.
|
13
|
-
#
|
14
|
-
# @param [AttributeMetadata] attr_md the attribute to check
|
15
|
-
def infer_attribute_inverse(attr_md)
|
16
|
-
inv = attr_md.type.detect_inverse_attribute(self)
|
17
|
-
if inv then
|
18
|
-
logger.debug { "Detected #{qp} #{attr_md} inverse #{inv.qp}." }
|
19
|
-
set_attribute_inverse(attr_md.to_sym, inv)
|
20
|
-
else
|
21
|
-
logger.debug { "No inverse detected for #{qp} #{attr_md}." }
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# Sets the given bi-directional association attribute's inverse.
|
26
|
-
#
|
27
|
-
# @param [Symbol] attribute the subject attribute
|
28
|
-
# @param [Symbol] the attribute inverse
|
29
|
-
# @raise [TypeError] if the inverse type is incompatible with this Resource
|
30
|
-
def set_attribute_inverse(attribute, inverse)
|
31
|
-
attr_md = attribute_metadata(attribute)
|
32
|
-
# return if inverse is already set
|
33
|
-
return if attr_md.inverse == inverse
|
34
|
-
# the default inverse
|
35
|
-
inverse ||= attr_md.type.detect_inverse_attribute(self)
|
36
|
-
# the inverse attribute meta-data
|
37
|
-
inv_md = attr_md.type.attribute_metadata(inverse)
|
38
|
-
# if the attribute is the many side of a 1:M relation, then delegate to the one side.
|
39
|
-
if attr_md.collection? and not inv_md.collection? then
|
40
|
-
return attr_md.type.set_attribute_inverse(inverse, attribute)
|
41
|
-
end
|
42
|
-
# this class must be the same as or a subclass of the inverse attribute type
|
43
|
-
unless self <= inv_md.type then
|
44
|
-
raise TypeError.new("Cannot set #{qp}.#{attribute} inverse to #{attr_md.type.qp}.#{attribute} with incompatible type #{inv_md.type.qp}")
|
45
|
-
end
|
46
|
-
|
47
|
-
# if the attribute is not declared by this class, then make a new attribute
|
48
|
-
# metadata specialized for this class.
|
49
|
-
unless attr_md.declarer == self then
|
50
|
-
attr_md = attr_md.dup
|
51
|
-
attr_md.declarer = self
|
52
|
-
add_attribute_metadata(attribute, inverse)
|
53
|
-
logger.debug { "Copied #{attr_md.declarer}.#{attribute} to #{qp} with restricted inverse return type #{qp}." }
|
54
|
-
end
|
55
|
-
# Set the inverse in the attribute metadata.
|
56
|
-
attr_md.inverse = inverse
|
57
|
-
|
58
|
-
# If attribute is the one side of a 1:M or non-reflexive 1:1 relation, then add the inverse updater.
|
59
|
-
unless attr_md.collection? then
|
60
|
-
add_inverse_updater(attribute, inverse)
|
61
|
-
unless attr_md.type == inv_md.type or inv_md.collection? then
|
62
|
-
attr_md.type.delegate_writer_to_inverse(inverse, attribute)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Detects an unambiguous attribute which refers to the given referencing class.
|
68
|
-
# If there is exactly one attribute with the given return type, then that attribute is chosen.
|
69
|
-
# Otherwise, the attribute whose name matches the underscored referencing class name is chosen,
|
70
|
-
# if any.
|
71
|
-
#
|
72
|
-
# @param [Class] klass the referencing class
|
73
|
-
# @return [Symbol, nil] the inverse attribute for the given referencing class and inverse,
|
74
|
-
# or nil if no owner attribute was detected
|
75
|
-
def detect_inverse_attribute(klass)
|
76
|
-
# the candidate attributes which return the referencing type
|
77
|
-
candidates = domain_attributes.compose { |attr_md| klass <= attr_md.type }
|
78
|
-
attr = detect_inverse_attribute_from_candidates(klass, candidates)
|
79
|
-
if attr then
|
80
|
-
logger.debug { "#{qp} #{klass.qp} inverse attribute is #{attr}." }
|
81
|
-
else
|
82
|
-
logger.debug { "#{qp} #{klass.qp} inverse attribute was not detected." }
|
83
|
-
end
|
84
|
-
attr
|
85
|
-
end
|
86
|
-
|
87
|
-
# Redefines the attribute writer method to delegate to its inverse writer.
|
88
|
-
# This is done to enforce inverse integrity.
|
89
|
-
#
|
90
|
-
# For a +Person+ attribute +account+ with inverse +holder+, this is equivalent to the following:
|
91
|
-
# class Person
|
92
|
-
# alias :set_account :account=
|
93
|
-
# def account=(acct)
|
94
|
-
# acct.holder = self if acct
|
95
|
-
# set_account(acct)
|
96
|
-
# end
|
97
|
-
# end
|
98
|
-
def delegate_writer_to_inverse(attribute, inverse)
|
99
|
-
attr_md = attribute_metadata(attribute)
|
100
|
-
# nothing to do if no inverse
|
101
|
-
inv_attr_md = attr_md.inverse_attribute_metadata || return
|
102
|
-
logger.debug { "Delegating #{qp}.#{attribute} update to the inverse #{attr_md.type.qp}.#{inv_attr_md}..." }
|
103
|
-
# redefine the write to set the dependent inverse
|
104
|
-
redefine_method(attr_md.writer) do |old_writer|
|
105
|
-
# delegate to the CaRuby::Resource set_inverse method
|
106
|
-
lambda { |dep| set_inverse(dep, old_writer, inv_attr_md.writer) }
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
private
|
111
|
-
|
112
|
-
# @param klass (see #detect_inverse_attribute)
|
113
|
-
# @param [<Symbol>] candidates the attributes constrained to the target type
|
114
|
-
# @return (see #detect_inverse_attribute)
|
115
|
-
def detect_inverse_attribute_from_candidates(klass, candidates)
|
116
|
-
return if candidates.empty?
|
117
|
-
# there can be at most one owner attribute per owner.
|
118
|
-
return candidates.first.to_sym if candidates.size == 1
|
119
|
-
# by convention, if more than one attribute references the owner type,
|
120
|
-
# then the attribute named after the owner type is the owner attribute
|
121
|
-
tgt = klass.name[/\w+$/].underscore.to_sym
|
122
|
-
tgt if candidates.detect { |attr| attr == tgt }
|
123
|
-
end
|
124
|
-
|
125
|
-
# Modifies the given attribute writer method if necessary to update the given inverse_attr value.
|
126
|
-
# This method is called on dependent and attributes qualified as inversible.
|
127
|
-
#
|
128
|
-
# @see #set_attribute_inverse
|
129
|
-
def add_inverse_updater(attribute, inverse)
|
130
|
-
attr_md = attribute_metadata(attribute)
|
131
|
-
# the reader and writer methods
|
132
|
-
rdr, wtr = attr_md.accessors
|
133
|
-
logger.debug { "Injecting inverse #{inverse} updater into #{qp}.#{attribute} writer method #{wtr}..." }
|
134
|
-
# the inverse atttribute metadata
|
135
|
-
inv_attr_md = attr_md.inverse_attribute_metadata
|
136
|
-
# the inverse attribute reader and writer
|
137
|
-
inv_rdr, inv_wtr = inv_accessors = inv_attr_md.accessors
|
138
|
-
|
139
|
-
# Redefine the writer method to update the inverse by delegating to the inverse
|
140
|
-
redefine_method(wtr) do |old_wtr|
|
141
|
-
# the attribute reader and (superseded) writer
|
142
|
-
accessors = [rdr, old_wtr]
|
143
|
-
if inv_attr_md.collection? then
|
144
|
-
lambda { |other| add_to_inverse_collection(other, accessors, inv_rdr) }
|
145
|
-
else
|
146
|
-
lambda { |other| set_inversible_noncollection_attribute(other, accessors, inv_wtr) }
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
@@ -1,155 +0,0 @@
|
|
1
|
-
require 'caruby/util/collection'
|
2
|
-
require 'caruby/import/java'
|
3
|
-
require 'caruby/domain/java_attribute_metadata'
|
4
|
-
require 'caruby/domain/resource_introspection'
|
5
|
-
require 'caruby/domain/resource_inverse'
|
6
|
-
require 'caruby/domain/resource_dependency'
|
7
|
-
require 'caruby/domain/resource_attributes'
|
8
|
-
|
9
|
-
module CaRuby
|
10
|
-
# Exception raised if a meta-data setting is missing or invalid.
|
11
|
-
class MetadataError < RuntimeError; end
|
12
|
-
|
13
|
-
# Adds introspected metadata to a Class.
|
14
|
-
module ResourceMetadata
|
15
|
-
include ResourceIntrospection, ResourceInverse, ResourceDependency, ResourceAttributes
|
16
|
-
|
17
|
-
attr_reader :domain_module
|
18
|
-
|
19
|
-
# JRuby 1.6 alert - if a concrete class implements a constructor, then calling super in an
|
20
|
-
# included module initialize method results in a recursive call back into that initialize.
|
21
|
-
# The work-around is to redefine each domain class new method to call the initalize_content
|
22
|
-
# method instead. All Resource domain classes or included modules must not override
|
23
|
-
# initialize. They can implement initialize_content instead.
|
24
|
-
def self.extended(klass)
|
25
|
-
klass.class_eval do
|
26
|
-
class << self
|
27
|
-
def new(opts=nil)
|
28
|
-
obj = super()
|
29
|
-
obj.merge_attributes(opts) if opts
|
30
|
-
obj
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# Associates meta-data with a Resource class. When a Resource class is added to a ResourceModule,
|
37
|
-
# the ResourceModule extends the Resource class with ResourceMetadata and calls this
|
38
|
-
# {#add_metadata} method with itself as the mod argument. The ResourceModule is accessed
|
39
|
-
# by the {#domain_module} method.
|
40
|
-
def add_metadata(mod)
|
41
|
-
@domain_module = mod
|
42
|
-
introspect # in ResourceIntrospection
|
43
|
-
end
|
44
|
-
|
45
|
-
# @return the domain type for attribute, or nil if attribute is not a domain attribute
|
46
|
-
def domain_type(attribute)
|
47
|
-
attr_md = attribute_metadata(attribute)
|
48
|
-
attr_md.type if attr_md.domain?
|
49
|
-
end
|
50
|
-
|
51
|
-
# Returns an empty value for the given attribute.
|
52
|
-
# * If this class is not abstract, then the empty value is the initialized value.
|
53
|
-
# * Otherwise, if the attribute is a Java primitive number then zero.
|
54
|
-
# * Otherwise, if the attribute is a Java primitive boolean then +false+.
|
55
|
-
# * Otherwise, the empty value is nil.
|
56
|
-
#
|
57
|
-
# @param [Symbol] attribute the target attribute
|
58
|
-
# @return [Numeric, Boolean, Enumerable, nil] the empty attribute value
|
59
|
-
def empty_value(attribute)
|
60
|
-
if abstract? then
|
61
|
-
attr_md = attribute_metadata(attribute)
|
62
|
-
# the Java property type
|
63
|
-
jtype = attr_md.property_descriptor.property_type if JavaAttributeMetadata === attr_md
|
64
|
-
# A primitive is either a boolean or a number (String is not primitive).
|
65
|
-
if jtype and jtype.primitive? then
|
66
|
-
type.name == 'boolean' ? false : 0
|
67
|
-
end
|
68
|
-
else
|
69
|
-
# Since this class is not abstract, create a prototype instance on demand and make
|
70
|
-
# a copy of the initialized collection value from that instance.
|
71
|
-
@prototype ||= new
|
72
|
-
value = @prototype.send(attribute) || return
|
73
|
-
value.class.new
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Prints this classifier's content to the log.
|
78
|
-
def pretty_print(q)
|
79
|
-
# KLUDGE - if not inited then bail.
|
80
|
-
# TODO - isolate when this occurs.
|
81
|
-
if @attr_md_hash.nil? then
|
82
|
-
q.text(qp)
|
83
|
-
return
|
84
|
-
end
|
85
|
-
|
86
|
-
# the Java property descriptors
|
87
|
-
property_descriptors = java_attributes.wrap { |attr| attribute_metadata(attr).property_descriptor }
|
88
|
-
# build a map of relevant display label => attributes
|
89
|
-
prop_printer = property_descriptors.wrap { |pd| PROP_DESC_PRINTER.wrap(pd) }
|
90
|
-
prop_syms = property_descriptors.map { |pd| pd.name.to_sym }.to_set
|
91
|
-
aliases = @alias_std_attr_map.keys - attributes.to_a - prop_syms
|
92
|
-
alias_attr_hash = aliases.to_compact_hash { |aliaz| @alias_std_attr_map[aliaz] }
|
93
|
-
dependents_printer = dependent_attributes.wrap { |attr| DEPENDENT_ATTR_PRINTER.wrap(attribute_metadata(attr)) }
|
94
|
-
owner_printer = owners.wrap { |type| TYPE_PRINTER.wrap(type) }
|
95
|
-
inverses = @attributes.to_compact_hash do |attr|
|
96
|
-
attr_md = attribute_metadata(attr)
|
97
|
-
"#{attr_md.type.qp}.#{attr_md.inverse}" if attr_md.inverse
|
98
|
-
end
|
99
|
-
domain_attr_printer = domain_attributes.to_compact_hash { |attr| domain_type(attr).qp }
|
100
|
-
map = {
|
101
|
-
"Java properties" => prop_printer,
|
102
|
-
"standard attributes" => attributes,
|
103
|
-
"aliases to standard attributes" => alias_attr_hash,
|
104
|
-
"secondary key" => secondary_key_attributes,
|
105
|
-
"mandatory attributes" => mandatory_attributes,
|
106
|
-
"domain attributes" => domain_attr_printer,
|
107
|
-
"creatable domain attributes" => creatable_domain_attributes,
|
108
|
-
"updatable domain attributes" => updatable_domain_attributes,
|
109
|
-
"fetched domain attributes" => fetched_domain_attributes,
|
110
|
-
"cascaded domain attributes" => cascaded_attributes,
|
111
|
-
"owners" => owner_printer,
|
112
|
-
"owner attributes" => owner_attributes,
|
113
|
-
"inverse attributes" => inverses,
|
114
|
-
"dependent attributes" => dependents_printer,
|
115
|
-
"default values" => defaults
|
116
|
-
}.delete_if { |key, value| value.nil_or_empty? }
|
117
|
-
|
118
|
-
# one indented line per entry, all but the last line ending in a comma
|
119
|
-
content = map.map { |label, value| " #{label}=>#{format_print_value(value)}" }.join(",\n")
|
120
|
-
# print the content to the log
|
121
|
-
q.text("#{qp} structure:\n#{content}")
|
122
|
-
end
|
123
|
-
|
124
|
-
protected
|
125
|
-
|
126
|
-
def self.extend_class(klass, mod)
|
127
|
-
klass.extend(self)
|
128
|
-
klass.add_metadata(mod)
|
129
|
-
end
|
130
|
-
|
131
|
-
private
|
132
|
-
|
133
|
-
# A proc to print the unqualified class name.
|
134
|
-
TYPE_PRINTER = PrintWrapper.new { |type| type.qp }
|
135
|
-
|
136
|
-
DEPENDENT_ATTR_PRINTER = PrintWrapper.new do |attr_md|
|
137
|
-
flags = []
|
138
|
-
flags << :logical if attr_md.logical?
|
139
|
-
flags << :autogenerated if attr_md.autogenerated?
|
140
|
-
flags << :disjoint if attr_md.disjoint?
|
141
|
-
flags.empty? ? "#{attr_md}" : "#{attr_md}(#{flags.join(',')})"
|
142
|
-
end
|
143
|
-
|
144
|
-
# A proc to print the property descriptor name.
|
145
|
-
PROP_DESC_PRINTER = PrintWrapper.new { |pd| pd.name }
|
146
|
-
|
147
|
-
def format_print_value(value)
|
148
|
-
case value
|
149
|
-
when String then value
|
150
|
-
when Class then value.qp
|
151
|
-
else value.pp_s(:single_line)
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|