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.
Files changed (61) hide show
  1. data/History.md +48 -0
  2. data/lib/caruby/cli/command.rb +2 -1
  3. data/lib/caruby/csv/csv_mapper.rb +8 -8
  4. data/lib/caruby/database/persistable.rb +44 -65
  5. data/lib/caruby/database/persistence_service.rb +12 -9
  6. data/lib/caruby/database/persistifier.rb +14 -14
  7. data/lib/caruby/database/reader.rb +53 -51
  8. data/lib/caruby/database/search_template_builder.rb +9 -10
  9. data/lib/caruby/database/store_template_builder.rb +58 -58
  10. data/lib/caruby/database/writer.rb +96 -96
  11. data/lib/caruby/database.rb +19 -19
  12. data/lib/caruby/domain/attribute.rb +581 -0
  13. data/lib/caruby/domain/attributes.rb +615 -0
  14. data/lib/caruby/domain/dependency.rb +240 -0
  15. data/lib/caruby/domain/importer.rb +183 -0
  16. data/lib/caruby/domain/introspection.rb +176 -0
  17. data/lib/caruby/domain/inverse.rb +173 -0
  18. data/lib/caruby/domain/inversible.rb +1 -2
  19. data/lib/caruby/domain/java_attribute.rb +173 -0
  20. data/lib/caruby/domain/merge.rb +13 -10
  21. data/lib/caruby/domain/metadata.rb +141 -0
  22. data/lib/caruby/domain/mixin.rb +35 -0
  23. data/lib/caruby/domain/reference_visitor.rb +5 -3
  24. data/lib/caruby/domain.rb +340 -0
  25. data/lib/caruby/import/java.rb +29 -25
  26. data/lib/caruby/migration/migratable.rb +5 -5
  27. data/lib/caruby/migration/migrator.rb +19 -15
  28. data/lib/caruby/migration/resource_module.rb +1 -1
  29. data/lib/caruby/resource.rb +39 -30
  30. data/lib/caruby/util/collection.rb +94 -33
  31. data/lib/caruby/util/coordinate.rb +28 -2
  32. data/lib/caruby/util/log.rb +4 -4
  33. data/lib/caruby/util/module.rb +12 -28
  34. data/lib/caruby/util/partial_order.rb +9 -10
  35. data/lib/caruby/util/pretty_print.rb +46 -26
  36. data/lib/caruby/util/topological_sync_enumerator.rb +10 -4
  37. data/lib/caruby/util/transitive_closure.rb +2 -2
  38. data/lib/caruby/util/visitor.rb +1 -1
  39. data/lib/caruby/version.rb +1 -1
  40. data/test/lib/caruby/database/persistable_test.rb +1 -1
  41. data/test/lib/caruby/domain/domain_test.rb +14 -28
  42. data/test/lib/caruby/domain/inversible_test.rb +1 -1
  43. data/test/lib/caruby/import/java_test.rb +5 -0
  44. data/test/lib/caruby/migration/test_case.rb +0 -1
  45. data/test/lib/caruby/test_case.rb +9 -10
  46. data/test/lib/caruby/util/collection_test.rb +23 -5
  47. data/test/lib/caruby/util/module_test.rb +10 -14
  48. data/test/lib/caruby/util/partial_order_test.rb +16 -15
  49. data/test/lib/caruby/util/visitor_test.rb +1 -1
  50. data/test/lib/examples/galena/clinical_trials/migration/test_case.rb +1 -1
  51. metadata +16 -15
  52. data/History.txt +0 -44
  53. data/lib/caruby/domain/attribute_metadata.rb +0 -551
  54. data/lib/caruby/domain/java_attribute_metadata.rb +0 -183
  55. data/lib/caruby/domain/resource_attributes.rb +0 -565
  56. data/lib/caruby/domain/resource_dependency.rb +0 -217
  57. data/lib/caruby/domain/resource_introspection.rb +0 -160
  58. data/lib/caruby/domain/resource_inverse.rb +0 -151
  59. data/lib/caruby/domain/resource_metadata.rb +0 -155
  60. data/lib/caruby/domain/resource_module.rb +0 -370
  61. 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 oldval == newval
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
@@ -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
- # {ResourceAttributes#mergeable_attributes}.
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 {ResourceAttributes#nondomain_attributes})
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
- inv_md = attr_md.inverse_attribute_metadata
113
- if inv_md and not inv_md.collection? then
114
- owtr = inv_md.writer
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
- # the target is either a source match or the source itself
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 alert - Java TreeSet comparison uses the TreeSet comparator rather than an
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 {ResourceAttributes#saved_domain_attributes}
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, &block)
396
+ def visit(source)
395
397
  target = @copier.call(source)
396
- super(source, target, &block)
398
+ super(source, target)
397
399
  end
398
400
  end
399
401