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