caruby-core 1.4.9 → 1.5.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/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
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'caruby/import/java'
|
2
|
+
require 'caruby/domain/java_attribute'
|
3
|
+
|
4
|
+
module CaRuby
|
5
|
+
module Domain
|
6
|
+
# Meta-data mix-in to infer and set inverse attributes.
|
7
|
+
module Inverse
|
8
|
+
|
9
|
+
# Returns the inverse of the given attribute. If the attribute has an #{Attribute#inverse_metadata},
|
10
|
+
# then that attribute's inverse is returned. Otherwise, if the attribute is an #{Attribute#owner?},
|
11
|
+
# then the target class dependent attribute which matches this type is returned, if it exists.
|
12
|
+
#
|
13
|
+
# @param [Attribute] attr_md the subject attribute
|
14
|
+
# @param [Class, nil] klass the target class
|
15
|
+
# @return [Attribute, nil] the inverse attribute, if any
|
16
|
+
def inverse_attribute_metadata(attr_md, klass=nil)
|
17
|
+
inv_md = attr_md.inverse_metadata
|
18
|
+
return inv_md if inv_md
|
19
|
+
if attr_md.dependent? and klass then
|
20
|
+
klass.owner_attribute_metadata_hash.each { |otype, oattr_md|
|
21
|
+
return oattr_md if self <= otype }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
# Infers the inverse of the given attribute declared by this class. A domain attribute is
|
28
|
+
# recognized as an inverse according to the {Inverse#detect_inverse_attribute}
|
29
|
+
# criterion.
|
30
|
+
#
|
31
|
+
# @param [Attribute] attr_md the attribute to check
|
32
|
+
def infer_attribute_inverse(attr_md)
|
33
|
+
inv = attr_md.type.detect_inverse_attribute(self)
|
34
|
+
if inv then set_attribute_inverse(attr_md.to_sym, inv) end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sets the given bi-directional association attribute's inverse.
|
38
|
+
#
|
39
|
+
# @param [Symbol] attribute the subject attribute
|
40
|
+
# @param [Symbol] the attribute inverse
|
41
|
+
# @raise [TypeError] if the inverse type is incompatible with this Resource
|
42
|
+
def set_attribute_inverse(attribute, inverse)
|
43
|
+
attr_md = attribute_metadata(attribute)
|
44
|
+
# return if inverse is already set
|
45
|
+
return if attr_md.inverse == inverse
|
46
|
+
# the default inverse
|
47
|
+
inverse ||= attr_md.type.detect_inverse_attribute(self)
|
48
|
+
# the inverse attribute meta-data
|
49
|
+
inv_md = attr_md.type.attribute_metadata(inverse)
|
50
|
+
# If the attribute is the many side of a 1:M relation, then delegate to the one side.
|
51
|
+
if attr_md.collection? and not inv_md.collection? then
|
52
|
+
return attr_md.type.set_attribute_inverse(inverse, attribute)
|
53
|
+
end
|
54
|
+
# This class must be the same as or a subclass of the inverse attribute type.
|
55
|
+
unless self <= inv_md.type then
|
56
|
+
raise TypeError.new("Cannot set #{qp}.#{attribute} inverse to #{attr_md.type.qp}.#{attribute} with incompatible type #{inv_md.type.qp}")
|
57
|
+
end
|
58
|
+
# If the attribute is not declared by this class, then make a new attribute
|
59
|
+
# metadata specialized for this class.
|
60
|
+
unless attr_md.declarer == self then
|
61
|
+
attr_md = restrict_attribute_inverse(attr_md, inverse)
|
62
|
+
end
|
63
|
+
# Set the inverse in the attribute metadata.
|
64
|
+
attr_md.inverse = inverse
|
65
|
+
# If attribute is the one side of a 1:M or non-reflexive 1:1 relation, then add the inverse updater.
|
66
|
+
unless attr_md.collection? then
|
67
|
+
# Make the
|
68
|
+
add_inverse_updater(attribute, inverse)
|
69
|
+
unless attr_md.type == inv_md.type or inv_md.collection? then
|
70
|
+
attr_md.type.delegate_writer_to_inverse(inverse, attribute)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Detects an unambiguous attribute which refers to the given referencing class.
|
76
|
+
# If there is exactly one attribute with the given return type, then that attribute is chosen.
|
77
|
+
# Otherwise, the attribute whose name matches the underscored referencing class name is chosen,
|
78
|
+
# if any.
|
79
|
+
#
|
80
|
+
# @param [Class] klass the referencing class
|
81
|
+
# @return [Symbol, nil] the inverse attribute for the given referencing class and inverse,
|
82
|
+
# or nil if no owner attribute was detected
|
83
|
+
def detect_inverse_attribute(klass)
|
84
|
+
# The candidate attributes return the referencing type and don't already have an inverse.
|
85
|
+
candidates = domain_attributes.compose { |attr_md| klass <= attr_md.type and attr_md.inverse.nil? }
|
86
|
+
attr = detect_inverse_attribute_from_candidates(klass, candidates)
|
87
|
+
if attr then
|
88
|
+
logger.debug { "#{qp} #{klass.qp} inverse attribute is #{attr}." }
|
89
|
+
else
|
90
|
+
logger.debug { "#{qp} #{klass.qp} inverse attribute was not detected." }
|
91
|
+
end
|
92
|
+
attr
|
93
|
+
end
|
94
|
+
|
95
|
+
# Redefines the attribute writer method to delegate to its inverse writer.
|
96
|
+
# This is done to enforce inverse integrity.
|
97
|
+
#
|
98
|
+
# For a +Person+ attribute +account+ with inverse +holder+, this is equivalent to the following:
|
99
|
+
# class Person
|
100
|
+
# alias :set_account :account=
|
101
|
+
# def account=(acct)
|
102
|
+
# acct.holder = self if acct
|
103
|
+
# set_account(acct)
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
def delegate_writer_to_inverse(attribute, inverse)
|
107
|
+
attr_md = attribute_metadata(attribute)
|
108
|
+
# nothing to do if no inverse
|
109
|
+
inv_attr_md = attr_md.inverse_metadata || return
|
110
|
+
logger.debug { "Delegating #{qp}.#{attribute} update to the inverse #{attr_md.type.qp}.#{inv_attr_md}..." }
|
111
|
+
# redefine the write to set the dependent inverse
|
112
|
+
redefine_method(attr_md.writer) do |old_writer|
|
113
|
+
# delegate to the CaRuby::Resource set_inverse method
|
114
|
+
lambda { |dep| set_inverse(dep, old_writer, inv_attr_md.writer) }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
# Copies the given attribute metadata from its declarer to this class. The new attribute metadata
|
121
|
+
# has the same attribute access methods, but the declarer is this class and the inverse is the
|
122
|
+
# given inverse attribute.
|
123
|
+
#
|
124
|
+
# @param [Attribute] attr_md the attribute to copy
|
125
|
+
# @param [Symbol] the attribute inverse
|
126
|
+
# @return [Attribute] the copied attribute metadata
|
127
|
+
def restrict_attribute_inverse(attr_md, inverse)
|
128
|
+
rst_attr_md = attr_md.dup
|
129
|
+
rst_attr_md.declarer = self
|
130
|
+
add_attribute_metadata(rst_attr_md)
|
131
|
+
logger.debug { "Copied #{attr_md.declarer}.#{attr_md} to #{qp} with inverse #{inverse}." }
|
132
|
+
rst_attr_md
|
133
|
+
end
|
134
|
+
|
135
|
+
# @param klass (see #detect_inverse_attribute)
|
136
|
+
# @param [<Symbol>] candidates the attributes constrained to the target type
|
137
|
+
# @return (see #detect_inverse_attribute)
|
138
|
+
def detect_inverse_attribute_from_candidates(klass, candidates)
|
139
|
+
return if candidates.empty?
|
140
|
+
# there can be at most one owner attribute per owner.
|
141
|
+
return candidates.first.to_sym if candidates.size == 1
|
142
|
+
# by convention, if more than one attribute references the owner type,
|
143
|
+
# then the attribute named after the owner type is the owner attribute
|
144
|
+
tgt = klass.name[/\w+$/].underscore.to_sym
|
145
|
+
tgt if candidates.detect { |attr| attr == tgt }
|
146
|
+
end
|
147
|
+
|
148
|
+
# Modifies the given attribute writer method to update the given inverse.
|
149
|
+
#
|
150
|
+
# @param (see #set_attribute_inverse)
|
151
|
+
def add_inverse_updater(attribute, inverse)
|
152
|
+
attr_md = attribute_metadata(attribute)
|
153
|
+
# the reader and writer methods
|
154
|
+
rdr, wtr = attr_md.accessors
|
155
|
+
logger.debug { "Injecting inverse #{inverse} updater into #{qp}.#{attribute} writer method #{wtr}..." }
|
156
|
+
# the inverse atttribute metadata
|
157
|
+
inv_attr_md = attr_md.inverse_metadata
|
158
|
+
# the inverse attribute reader and writer
|
159
|
+
inv_rdr, inv_wtr = inv_accessors = inv_attr_md.accessors
|
160
|
+
# Redefine the writer method to update the inverse by delegating to the inverse
|
161
|
+
redefine_method(wtr) do |old_wtr|
|
162
|
+
# the attribute reader and (superseded) writer
|
163
|
+
accessors = [rdr, old_wtr]
|
164
|
+
if inv_attr_md.collection? then
|
165
|
+
lambda { |other| add_to_inverse_collection(other, accessors, inv_rdr) }
|
166
|
+
else
|
167
|
+
lambda { |other| set_inversible_noncollection_attribute(other, accessors, inv_wtr) }
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -60,14 +60,13 @@ module CaRuby
|
|
60
60
|
# the current inverse
|
61
61
|
oldval = send(rdr)
|
62
62
|
# no-op if no change
|
63
|
-
return newval if
|
63
|
+
return newval if newval == oldval
|
64
64
|
|
65
65
|
# delete self from the current inverse reference collection
|
66
66
|
if oldval then
|
67
67
|
coll = oldval.send(inverse)
|
68
68
|
coll.delete(self) if coll
|
69
69
|
end
|
70
|
-
|
71
70
|
# call the writer on this object
|
72
71
|
send(wtr, newval)
|
73
72
|
# add self to the inverse collection
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'caruby/util/inflector'
|
2
|
+
require 'caruby/domain/attribute'
|
3
|
+
|
4
|
+
module CaRuby
|
5
|
+
module Domain
|
6
|
+
# The attribute metadata for an introspected Java property.
|
7
|
+
class JavaAttribute < Attribute
|
8
|
+
|
9
|
+
# This attribute's Java property descriptor.
|
10
|
+
attr_reader :property_descriptor
|
11
|
+
|
12
|
+
# This attribute's Java property [reader, writer] accessors, e.g. +[:getActivityStatus, :setActivityStatus]+.
|
13
|
+
attr_reader :property_accessors
|
14
|
+
|
15
|
+
# Creates a Ruby Attribute symbol corresponding to the given Ruby Java class wrapper klazz
|
16
|
+
# and Java property_descriptor.
|
17
|
+
#
|
18
|
+
# The attribute name is the lower-case, underscore property descriptor name with the alterations
|
19
|
+
# described in {JavaAttribute.to_attribute_symbol} and {Class#unocclude_reserved_method}.
|
20
|
+
#
|
21
|
+
# The attribute type is inferred as follows:
|
22
|
+
# * If the property descriptor return type is a primitive Java type, then that type is returned.
|
23
|
+
# * If the return type is a parameterized collection, then the parameter type is returned.
|
24
|
+
# * If the return type is an unparameterized collection, then this method infers the type from
|
25
|
+
# the property name, e.g. +StudyProtocolCollection+type is inferred as +StudyProtocol+
|
26
|
+
# by stripping the +Collection+ suffix, capitalizing the prefix and looking for a class of
|
27
|
+
# that name in the {Metadata#domain_module}.
|
28
|
+
# * If the declarer class metadata configuration includes a +domain_attributes+ property, then
|
29
|
+
# the type specified in that property is returned.
|
30
|
+
# * Otherwise, this method returns Java::Javalang::Object.
|
31
|
+
#
|
32
|
+
# The optional restricted_type argument restricts the attribute to a subclass of the declared
|
33
|
+
# property type.
|
34
|
+
def initialize(pd, declarer, restricted_type=nil)
|
35
|
+
symbol = create_standard_attribute_symbol(pd, declarer)
|
36
|
+
super(symbol, declarer, restricted_type)
|
37
|
+
@property_descriptor = pd
|
38
|
+
# deficient Java introspector does not recognize 'is' prefix for a Boolean property
|
39
|
+
rm = declarer.property_read_method(pd)
|
40
|
+
raise ArgumentError.new("Property does not have a read method: #{declarer.qp}.#{pd.name}") unless rm
|
41
|
+
reader = rm.name.to_sym
|
42
|
+
unless declarer.method_defined?(reader) then
|
43
|
+
reader = "is#{reader.to_s.capitalize_first}".to_sym
|
44
|
+
unless declarer.method_defined?(reader) then
|
45
|
+
raise ArgumentError.new("Reader method not found for #{declarer} property #{pd.name}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
unless pd.write_method then
|
49
|
+
raise ArgumentError.new("Property does not have a write method: #{declarer.qp}.#{pd.name}")
|
50
|
+
end
|
51
|
+
writer = pd.write_method.name.to_sym
|
52
|
+
unless declarer.method_defined?(writer) then
|
53
|
+
raise ArgumentError.new("Writer method not found for #{declarer} property #{pd.name}")
|
54
|
+
end
|
55
|
+
@property_accessors = [reader, writer]
|
56
|
+
qualify(:collection) if collection_java_class?
|
57
|
+
@type = infer_type
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Symbol] the JRuby wrapper method for the Java property reader
|
61
|
+
def property_reader
|
62
|
+
property_accessors.first
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Symbol] the JRuby wrapper method for the Java property writer
|
66
|
+
def property_writer
|
67
|
+
property_accessors.last
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a lower-case, underscore symbol for the given property_name.
|
71
|
+
# A name ending in 'Collection' is changed to a pluralization.
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# JavaAttribute.to_attribute_symbol('specimenEventCollection') #=> :specimen_events
|
75
|
+
def self.to_attribute_symbol(property_name)
|
76
|
+
name = if property_name =~ /(.+)Collection$/ then
|
77
|
+
property_name[0...-'Collection'.length].pluralize.underscore
|
78
|
+
else
|
79
|
+
property_name.underscore
|
80
|
+
end
|
81
|
+
name.to_sym
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# @param pd the Java property descriptor
|
87
|
+
# @param [Class] klass the declarer
|
88
|
+
# @return [String] the lower-case, underscore symbol for the given property descriptor
|
89
|
+
def create_standard_attribute_symbol(pd, klass)
|
90
|
+
propname = pd.name
|
91
|
+
name = propname.underscore
|
92
|
+
renamed = klass.unocclude_reserved_method(pd)
|
93
|
+
if renamed then
|
94
|
+
logger.debug { "Renamed #{klass.qp} reserved Ruby method #{name} to #{renamed}." }
|
95
|
+
renamed
|
96
|
+
else
|
97
|
+
JavaAttribute.to_attribute_symbol(propname)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [Boolean] whether this property's Java type is +Iterable+
|
102
|
+
def collection_java_class?
|
103
|
+
# the Java property type
|
104
|
+
ptype = @property_descriptor.property_type
|
105
|
+
# Test whether the corresponding JRuby wrapper class or module is an Iterable.
|
106
|
+
Class.to_ruby(ptype) < Java::JavaLang::Iterable
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Class] the type for the specified klass property descriptor pd as described in {#initialize}
|
110
|
+
def infer_type
|
111
|
+
collection? ? infer_collection_type : infer_non_collection_type
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the domain type for this attribute's Java Collection property descriptor.
|
115
|
+
# If the property type is parameterized by a single domain class, then that generic type argument is the domain type.
|
116
|
+
# Otherwise, the type is inferred from the property name as described in {#infer_collection_type_from_name}.
|
117
|
+
#
|
118
|
+
# @return [Class] this property's Ruby type
|
119
|
+
def infer_collection_type
|
120
|
+
generic_parameter_type or infer_collection_type_from_name or Java::JavaLang::Object
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return [Class] this property's Ruby type
|
124
|
+
def infer_non_collection_type
|
125
|
+
jtype = @property_descriptor.property_type
|
126
|
+
Class.to_ruby(jtype)
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return [Class, nil] the domain type of this attribute's property descriptor Collection generic
|
130
|
+
# type argument, or nil if none
|
131
|
+
def generic_parameter_type
|
132
|
+
method = @property_descriptor.readMethod || return
|
133
|
+
gtype = method.genericReturnType
|
134
|
+
return unless Java::JavaLangReflect::ParameterizedType === gtype
|
135
|
+
atypes = gtype.actualTypeArguments
|
136
|
+
return unless atypes.size == 1
|
137
|
+
atype = atypes[0]
|
138
|
+
klass = java_to_ruby_class(atype)
|
139
|
+
logger.debug { "Inferred #{declarer.qp} #{self} domain type #{klass.qp} from generic parameter #{atype.name}." } if klass
|
140
|
+
klass
|
141
|
+
end
|
142
|
+
|
143
|
+
# @param [Class, String] jtype the Java class or class name
|
144
|
+
# @return [Class] the corresponding Ruby type
|
145
|
+
def java_to_ruby_class(jtype)
|
146
|
+
name = String === jtype ? jtype : jtype.name
|
147
|
+
Class.to_ruby(name)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns the domain type for this attribute's collection Java property descriptor name.
|
151
|
+
# By convention, caBIG domain collection properties often begin with a domain type
|
152
|
+
# name and end in 'Collection'. This method strips the Collection suffix and checks
|
153
|
+
# whether the prefix is a domain class.
|
154
|
+
#
|
155
|
+
# For example, the type of the property named +distributionProtocolCollection+
|
156
|
+
# is inferred as +DistributionProtocol+ by stripping the +Collection+ suffix,
|
157
|
+
# capitalizing the prefix and looking for a class of that name in this classifier's
|
158
|
+
# domain_module.
|
159
|
+
#
|
160
|
+
# @return [Class] the collection item type
|
161
|
+
def infer_collection_type_from_name
|
162
|
+
# the property name
|
163
|
+
pname = @property_descriptor.name
|
164
|
+
# The potential class name is the capitalized property name without a 'Collection' suffix.
|
165
|
+
cname = pname.capitalize_first.sub(/Collection$/, '')
|
166
|
+
jname = [@declarer.parent_module, cname].join('::')
|
167
|
+
klass = eval jname rescue nil
|
168
|
+
if klass then logger.debug { "Inferred #{declarer.qp} #{self} collection domain type #{klass.qp} from the attribute name." } end
|
169
|
+
klass
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/caruby/domain/merge.rb
CHANGED
@@ -20,13 +20,13 @@ module CaRuby
|
|
20
20
|
#
|
21
21
|
# If other is not a Hash, then the other object's attributes values are merged into
|
22
22
|
# this object. The default attributes is this mergeable's class
|
23
|
-
# {
|
23
|
+
# {Attributes#mergeable_attributes}.
|
24
24
|
#
|
25
25
|
# The merge is performed by calling {#merge_attribute} on each attribute with the matches
|
26
26
|
# and merger block given to this method.
|
27
27
|
#
|
28
28
|
# @param [Mergeable, {Symbol => Object}] other the source domain object or value hash to merge from
|
29
|
-
# @param [<Symbol>, nil] attributes the attributes to merge (default {
|
29
|
+
# @param [<Symbol>, nil] attributes the attributes to merge (default {Attributes#nondomain_attributes})
|
30
30
|
# @param [{Resource => Resource}, nil] the optional merge source => target reference matches
|
31
31
|
# @yield [attribute, oldval, newval] the optional merger block
|
32
32
|
# @yieldparam [Symbol] attribute the merge target attribute
|
@@ -76,7 +76,7 @@ module CaRuby
|
|
76
76
|
# @return the merged attribute value
|
77
77
|
def merge_attribute(attribute, newval, matches=nil)
|
78
78
|
# the previous value
|
79
|
-
oldval = send(attribute)
|
79
|
+
oldval = send(attribute)
|
80
80
|
# If nothing to merge or a block can take over, then bail.
|
81
81
|
if newval.nil? or mergeable__equal?(oldval, newval) then
|
82
82
|
return oldval
|
@@ -109,9 +109,13 @@ module CaRuby
|
|
109
109
|
# @see #merge_attribute
|
110
110
|
def merge_domain_attribute_value(attr_md, oldval, newval, matches)
|
111
111
|
# the dependent owner writer method, if any
|
112
|
-
|
113
|
-
|
114
|
-
|
112
|
+
if attr_md.dependent? then
|
113
|
+
val = attr_md.collection? ? newval.first : newval
|
114
|
+
klass = val.class if val
|
115
|
+
inv_md = self.class.inverse_attribute_metadata(attr_md, klass)
|
116
|
+
if inv_md and not inv_md.collection? then
|
117
|
+
owtr = inv_md.writer
|
118
|
+
end
|
115
119
|
end
|
116
120
|
|
117
121
|
# If the attribute is a collection, then merge the matches into the current attribute
|
@@ -161,9 +165,8 @@ module CaRuby
|
|
161
165
|
oldval.merge(newval)
|
162
166
|
else
|
163
167
|
# No target; set the attribute to the source.
|
164
|
-
#
|
165
|
-
ref = matches[newval] if matches
|
166
|
-
ref ||= newval
|
168
|
+
# The target is either a source match or the source itself.
|
169
|
+
ref = (matches[newval] if matches) || newval
|
167
170
|
logger.debug { "Setting #{qp} #{attr_md} reference #{ref.qp}..." }
|
168
171
|
# If the target is a dependent, then set the dependent owner, which will in turn
|
169
172
|
# set the attribute to the dependent. Otherwise, set the attribute to the target.
|
@@ -172,7 +175,7 @@ module CaRuby
|
|
172
175
|
newval
|
173
176
|
end
|
174
177
|
|
175
|
-
# Java
|
178
|
+
# Java Java TreeSet comparison uses the TreeSet comparator rather than an
|
176
179
|
# element-wise comparator. Work around this rare aberration by converting the TreeSet
|
177
180
|
# to a Ruby Set.
|
178
181
|
def mergeable__equal?(v1, v2)
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'caruby/util/collection'
|
2
|
+
require 'caruby/import/java'
|
3
|
+
require 'caruby/domain/java_attribute'
|
4
|
+
require 'caruby/domain/introspection'
|
5
|
+
require 'caruby/domain/inverse'
|
6
|
+
require 'caruby/domain/dependency'
|
7
|
+
require 'caruby/domain/attributes'
|
8
|
+
|
9
|
+
module CaRuby
|
10
|
+
module Domain
|
11
|
+
# Exception raised if a meta-data setting is missing or invalid.
|
12
|
+
class MetadataError < RuntimeError; end
|
13
|
+
|
14
|
+
# Adds introspected metadata to a Class.
|
15
|
+
module Metadata
|
16
|
+
include Introspection, Inverse, Dependency, Attributes
|
17
|
+
|
18
|
+
# @return [Module] the {Domain} module context
|
19
|
+
attr_accessor :domain_module
|
20
|
+
|
21
|
+
def self.extended(klass)
|
22
|
+
super
|
23
|
+
klass.class_eval do
|
24
|
+
# Add this class's metadata.
|
25
|
+
introspect
|
26
|
+
# Add the {attribute=>value} argument constructor.
|
27
|
+
class << self
|
28
|
+
def new(opts=nil)
|
29
|
+
obj = super()
|
30
|
+
obj.merge_attributes(opts) if opts
|
31
|
+
obj
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return the domain type for attribute, or nil if attribute is not a domain attribute
|
38
|
+
def domain_type(attribute)
|
39
|
+
attr_md = attribute_metadata(attribute)
|
40
|
+
attr_md.type if attr_md.domain?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns an empty value for the given attribute.
|
44
|
+
# * If this class is not abstract, then the empty value is the initialized value.
|
45
|
+
# * Otherwise, if the attribute is a Java primitive number then zero.
|
46
|
+
# * Otherwise, if the attribute is a Java primitive boolean then +false+.
|
47
|
+
# * Otherwise, the empty value is nil.
|
48
|
+
#
|
49
|
+
# @param [Symbol] attribute the target attribute
|
50
|
+
# @return [Numeric, Boolean, Enumerable, nil] the empty attribute value
|
51
|
+
def empty_value(attribute)
|
52
|
+
if abstract? then
|
53
|
+
attr_md = attribute_metadata(attribute)
|
54
|
+
# the Java property type
|
55
|
+
jtype = attr_md.property_descriptor.property_type if JavaAttribute === attr_md
|
56
|
+
# A primitive is either a boolean or a number (String is not primitive).
|
57
|
+
if jtype and jtype.primitive? then
|
58
|
+
type.name == 'boolean' ? false : 0
|
59
|
+
end
|
60
|
+
else
|
61
|
+
# Since this class is not abstract, create a prototype instance on demand and make
|
62
|
+
# a copy of the initialized collection value from that instance.
|
63
|
+
@prototype ||= new
|
64
|
+
value = @prototype.send(attribute) || return
|
65
|
+
value.class.new
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Prints this classifier's content to the log.
|
70
|
+
def pretty_print(q)
|
71
|
+
# the Java property descriptors
|
72
|
+
property_descriptors = java_attributes.wrap { |attr| attribute_metadata(attr).property_descriptor }
|
73
|
+
# build a map of relevant display label => attributes
|
74
|
+
prop_printer = property_descriptors.wrap { |pd| PROP_DESC_PRINTER.wrap(pd) }
|
75
|
+
prop_syms = property_descriptors.map { |pd| pd.name.to_sym }.to_set
|
76
|
+
aliases = @alias_std_attr_map.keys - attributes.to_a - prop_syms
|
77
|
+
alias_attr_hash = aliases.to_compact_hash { |aliaz| @alias_std_attr_map[aliaz] }
|
78
|
+
dependents_printer = dependent_attributes.wrap { |attr| DEPENDENT_ATTR_PRINTER.wrap(attribute_metadata(attr)) }
|
79
|
+
owner_printer = owners.wrap { |type| TYPE_PRINTER.wrap(type) }
|
80
|
+
inverses = @attributes.to_compact_hash do |attr|
|
81
|
+
attr_md = attribute_metadata(attr)
|
82
|
+
"#{attr_md.type.qp}.#{attr_md.inverse}" if attr_md.inverse
|
83
|
+
end
|
84
|
+
domain_attr_printer = domain_attributes.to_compact_hash { |attr| domain_type(attr).qp }
|
85
|
+
map = {
|
86
|
+
"Java properties" => prop_printer,
|
87
|
+
"standard attributes" => attributes,
|
88
|
+
"aliases to standard attributes" => alias_attr_hash,
|
89
|
+
"secondary key" => secondary_key_attributes,
|
90
|
+
"mandatory attributes" => mandatory_attributes,
|
91
|
+
"domain attributes" => domain_attr_printer,
|
92
|
+
"creatable domain attributes" => creatable_domain_attributes,
|
93
|
+
"updatable domain attributes" => updatable_domain_attributes,
|
94
|
+
"fetched domain attributes" => fetched_domain_attributes,
|
95
|
+
"cascaded domain attributes" => cascaded_attributes,
|
96
|
+
"owners" => owner_printer,
|
97
|
+
"owner attributes" => owner_attributes,
|
98
|
+
"inverse attributes" => inverses,
|
99
|
+
"dependent attributes" => dependents_printer,
|
100
|
+
"default values" => defaults
|
101
|
+
}.delete_if { |key, value| value.nil_or_empty? }
|
102
|
+
|
103
|
+
# one indented line per entry, all but the last line ending in a comma
|
104
|
+
content = map.map { |label, value| " #{label}=>#{format_print_value(value)}" }.join(",\n")
|
105
|
+
# print the content to the log
|
106
|
+
q.text("#{qp} structure:\n#{content}")
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
|
111
|
+
def self.extend_class(klass, mod)
|
112
|
+
klass.extend(self)
|
113
|
+
klass.add_metadata(mod)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# A proc to print the unqualified class name.
|
119
|
+
TYPE_PRINTER = PrintWrapper.new { |type| type.qp }
|
120
|
+
|
121
|
+
DEPENDENT_ATTR_PRINTER = PrintWrapper.new do |attr_md|
|
122
|
+
flags = []
|
123
|
+
flags << :logical if attr_md.logical?
|
124
|
+
flags << :autogenerated if attr_md.autogenerated?
|
125
|
+
flags << :disjoint if attr_md.disjoint?
|
126
|
+
flags.empty? ? "#{attr_md}" : "#{attr_md}(#{flags.join(',')})"
|
127
|
+
end
|
128
|
+
|
129
|
+
# A proc to print the property descriptor name.
|
130
|
+
PROP_DESC_PRINTER = PrintWrapper.new { |pd| pd.name }
|
131
|
+
|
132
|
+
def format_print_value(value)
|
133
|
+
case value
|
134
|
+
when String then value
|
135
|
+
when Class then value.qp
|
136
|
+
else value.pp_s(:single_line)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'caruby/domain/metadata'
|
2
|
+
|
3
|
+
module CaRuby
|
4
|
+
module Domain
|
5
|
+
# Mixin extends a module to add meta-data to included classes.
|
6
|
+
module Mixin
|
7
|
+
# Adds {Metadata} to an included class.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# module CaRuby
|
11
|
+
# module Resource
|
12
|
+
# def self.included(mod)
|
13
|
+
# mod.extend(Domain::Mixin)
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
# module ClinicalTrials
|
18
|
+
# module Resource
|
19
|
+
# include CaRuby::Resource
|
20
|
+
# end
|
21
|
+
# class Subject
|
22
|
+
# include Resource #=> introspects the Subject meta-data
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @param [Module] class_or_module the included module, usually a class
|
27
|
+
def included(class_or_module)
|
28
|
+
super
|
29
|
+
if Class === class_or_module then
|
30
|
+
Metadata.ensure_metadata_introspected(class_or_module)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -21,7 +21,7 @@ module CaRuby
|
|
21
21
|
# Creates a new ReferenceVisitor on domain reference attributes.
|
22
22
|
#
|
23
23
|
# If a selector block is given to this initializer, then the reference attributes to visit
|
24
|
-
# are determined by calling the block. Otherwise, the {
|
24
|
+
# are determined by calling the block. Otherwise, the {Attributes#saved_domain_attributes}
|
25
25
|
# are visited.
|
26
26
|
#
|
27
27
|
# @param options (see Visitor#initialize)
|
@@ -345,6 +345,8 @@ module CaRuby
|
|
345
345
|
# @param [Resource] target the domain object to merge into
|
346
346
|
# @return [Resource] the merged target
|
347
347
|
def merge(source, target)
|
348
|
+
# trivial case
|
349
|
+
return target if source.equal?(target)
|
348
350
|
# the domain attributes to merge
|
349
351
|
attrs = @mergeable.call(source)
|
350
352
|
logger.debug { format_merge_log_message(source, target, attrs) }
|
@@ -391,9 +393,9 @@ module CaRuby
|
|
391
393
|
# @param (see MergeVisitor#visit)
|
392
394
|
# @yield (see MergeVisitor#visit)
|
393
395
|
# @yieldparam (see MergeVisitor#visit)
|
394
|
-
def visit(source
|
396
|
+
def visit(source)
|
395
397
|
target = @copier.call(source)
|
396
|
-
super(source, target
|
398
|
+
super(source, target)
|
397
399
|
end
|
398
400
|
end
|
399
401
|
|