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