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,240 @@
1
+ require 'caruby/util/validation'
2
+
3
+ module CaRuby
4
+ module Domain
5
+ # Metadata mix-in to capture Resource dependency.
6
+ module Dependency
7
+
8
+ attr_reader :owners, :owner_attributes
9
+
10
+ # Returns the most specific attribute which references the dependent type, or nil if none.
11
+ # If the given class can be returned by more than dependent attribute, then the attribute
12
+ # is chosen whose return type most closely matches the given class.
13
+ #
14
+ # @param [Class] klass the dependent type
15
+ # @return [Symbol, nil] the dependent reference attribute, or nil if none
16
+ def dependent_attribute(klass)
17
+ dependent_attributes.inject(nil) do |best, attr|
18
+ type = domain_type(attr)
19
+ # If the attribute can return the klass then the return type is a candidate.
20
+ # In that case, the klass replaces the best candidate if it is more specific than
21
+ # the best candidate so far.
22
+ klass <= type ? (best && best < type ? best : type) : best
23
+ end
24
+ end
25
+
26
+ # Adds the given attribute as a dependent.
27
+ #
28
+ # Supported flags include the following:
29
+ # * :logical - the dependency relation is not cascaded by the application
30
+ # * :autogenerated - a dependent can be created by the application as a side-effect of creating the owner
31
+ # * :disjoint - the dependent owner has more than one owner attribute, but only one owner instance
32
+ #
33
+ # If the attribute inverse is not a collection, then the attribute writer
34
+ # is modified to delegate to the dependent owner writer. This enforces
35
+ # referential integrity by ensuring that the following post-condition holds:
36
+ # * _owner_._attribute_._inverse_ == _owner_
37
+ # where:
38
+ # * _owner_ is an instance this attribute's declaring class
39
+ # * _inverse_ is the owner inverse attribute defined in the dependent class
40
+ #
41
+ # @param [Symbol] attribute the dependent to add
42
+ # @param [<Symbol>] flags the attribute qualifier flags
43
+ def add_dependent_attribute(attribute, *flags)
44
+ attr_md = attribute_metadata(attribute)
45
+ flags << :dependent unless flags.include?(:dependent)
46
+ attr_md.qualify(*flags)
47
+ inverse = attr_md.inverse
48
+ inv_type = attr_md.type
49
+ # example: Parent.add_dependent_attribute(:children) with inverse :parent calls the following:
50
+ # Child.add_owner(Parent, :children, :parent)
51
+ inv_type.add_owner(self, attribute, inverse)
52
+ end
53
+
54
+ # Makes a new owner attribute. The attribute name is the lower-case demodulized
55
+ # owner class name. The owner class must reference this class via the given
56
+ # inverse dependent attribute.
57
+ #
58
+ # @param klass (see #detect_owner_attribute)
59
+ # @param [Symbol] the owner -> dependent inverse attribute
60
+ # @return [Symbol] this class's new owner attribute
61
+ # @raise [ArgumentError] if the inverse is nil
62
+ def create_owner_attribute(klass, inverse)
63
+ if inverse.nil? then
64
+ raise ArgumentError.new("Cannot create a #{qp} owner attribute to #{klass} without a dependent attribute to this class.")
65
+ end
66
+ attr = klass.name.demodulize.underscore.to_sym
67
+ attr_accessor(attr)
68
+ attr_md = add_attribute(attr, klass)
69
+ attr_md.inverse = inverse
70
+ logger.debug { "Created #{qp} owner attribute #{attr} with inverse #{klass.qp}.#{inverse}." }
71
+ attr
72
+ end
73
+
74
+ # @return [Boolean] whether this class depends on an owner
75
+ def dependent?
76
+ not owners.empty?
77
+ end
78
+
79
+ # @return [Boolean] whether this class has an owner which cascades save operations to this dependent
80
+ def cascaded_dependent?
81
+ owner_attribute_metadata_enumerator.any? { |attr_md| attr_md.inverse_metadata.cascaded? }
82
+ end
83
+
84
+ # @return [Boolean] whether this class depends the given other class
85
+ def depends_on?(other)
86
+ owners.detect { |owner| owner === other }
87
+ end
88
+
89
+ # @param [Class] klass the dependent type
90
+ # @return [Symbol, nil] the attribute which references the dependent type, or nil if none
91
+ def dependent_attribute(klass)
92
+ type = dependent_attributes.detect_with_metadata { |attr_md| attr_md.type == klass }
93
+ return type if type
94
+ dependent_attribute(klass.superclass) if klass.superclass < Resource
95
+ end
96
+
97
+ # @return [<Symbol>] this class's owner attributes
98
+ def owner_attributes
99
+ @oattrs ||= owner_attribute_metadata_enumerator.transform { |attr_md| attr_md.to_sym }
100
+ end
101
+
102
+ # @return [<Class>] this class's dependent types
103
+ def dependents
104
+ dependent_attributes.wrap { |attr| attr.type }
105
+ end
106
+
107
+ # @return [<Class>] this class's owner types
108
+ def owners
109
+ @owners ||= Enumerable::Enumerator.new(owner_attribute_metadata_hash, :each_key)
110
+ end
111
+
112
+ # @return [Attribute, nil] the sole owner attribute metadata of this class, or nil if there
113
+ # is not exactly one owner
114
+ def owner_attribute_metadata
115
+ attr_mds = owner_attribute_metadata_enumerator
116
+ attr_mds.first if attr_mds.size == 1
117
+ end
118
+
119
+ # @return [Symbol, nil] the sole owner attribute of this class, or nil if there
120
+ # is not exactly one owner
121
+ def owner_attribute
122
+ attr_md = owner_attribute_metadata || return
123
+ attr_md.to_sym
124
+ end
125
+
126
+ # @return [Class, nil] the sole owner type of this class, or nil if there
127
+ # is not exactly one owner
128
+ def owner_type
129
+ attr_md = owner_attribute_metadata || return
130
+ attr_md.type
131
+ end
132
+
133
+ protected
134
+
135
+ # Adds the given owner class to this dependent class.
136
+ # This method must be called before any dependent attribute is accessed.
137
+ # If the attribute is given, then the attribute inverse is set.
138
+ # Otherwise, if there is not already an owner attribute, then a new owner attribute is created.
139
+ # The name of the new attribute is the lower-case demodulized owner class name.
140
+ #
141
+ # @param [Class] the owner class
142
+ # @param [Symbol] inverse the owner -> dependent attribute
143
+ # @param [Symbol, nil] attribute the dependent -> owner attribute, if known
144
+ # @raise [ValidationError] if the inverse is nil
145
+ def add_owner(klass, inverse, attribute=nil)
146
+ if inverse.nil? then raise ValidationError.new("Owner #{klass.qp} missing dependent attribute for dependent #{qp}") end
147
+ logger.debug { "Adding #{qp} owner #{klass.qp}#{' attribute ' + attribute.to_s if attribute}#{' inverse ' + inverse.to_s if inverse}..." }
148
+ if @owner_attr_hash then
149
+ raise MetadataError.new("Can't add #{qp} owner #{klass.qp} after dependencies have been accessed")
150
+ end
151
+
152
+ # detect the owner attribute, if necessary
153
+ attribute ||= detect_owner_attribute(klass, inverse)
154
+ attr_md = attribute_metadata(attribute) if attribute
155
+ # Add the owner class => attribute entry.
156
+ # The attribute is nil if the dependency is unidirectional, i.e. there is an owner class which
157
+ # references this class via a dependency attribute but there is no inverse owner attribute.
158
+ local_owner_attribute_metadata_hash[klass] = attr_md
159
+ # If the dependency is unidirectional, then our job is done.
160
+ return if attribute.nil?
161
+
162
+ # set the inverse if necessary
163
+ unless attr_md.inverse then
164
+ set_attribute_inverse(attribute, inverse)
165
+ end
166
+ # set the owner flag if necessary
167
+ unless attr_md.owner? then attr_md.qualify(:owner) end
168
+ # Redefine the writer method to warn when changing the owner
169
+ rdr, wtr = attr_md.accessors
170
+ logger.debug { "Injecting owner change warning into #{qp}.#{attribute} writer method #{wtr}..." }
171
+ redefine_method(wtr) do |old_wtr|
172
+ lambda do |ref|
173
+ prev = send(rdr)
174
+ if prev and prev != ref then
175
+ if ref.nil? then
176
+ logger.warn("Unsetting the #{self} owner #{attribute} #{prev}.")
177
+ elsif ref.identifier != prev.identifier then
178
+ logger.warn("Resetting the #{self} owner #{attribute} from #{prev} to #{ref}.")
179
+ end
180
+ end
181
+ send(old_wtr, ref)
182
+ end
183
+ end
184
+ end
185
+
186
+ # Adds the given attribute as an owner. This method is called when a new attribute is added that
187
+ # references an existing owner.
188
+ #
189
+ # @param [Symbol] attribute the owner attribute
190
+ def add_owner_attribute(attribute)
191
+ attr_md = attribute_metadata(attribute)
192
+ otype = attr_md.type
193
+ hash = local_owner_attribute_metadata_hash
194
+ if hash.include?(otype) then
195
+ oattr = hash[otype]
196
+ unless oattr.nil? then
197
+ raise MetadataError.new("Cannot set #{qp} owner attribute to #{attribute} since it is already set to #{oattr}")
198
+ end
199
+ hash[otype] = attr_md
200
+ else
201
+ add_owner(otype, attr_md.inverse, attribute)
202
+ end
203
+ end
204
+
205
+ # @return [{Class => Attribute}] this class's owner type => attribute hash
206
+ def owner_attribute_metadata_hash
207
+ @oa_hash ||= create_owner_attribute_metadata_hash
208
+ end
209
+
210
+ private
211
+
212
+ def local_owner_attribute_metadata_hash
213
+ @local_oa_hash ||= {}
214
+ end
215
+
216
+ # @return [{Class => Attribute}] a new owner type => attribute hash
217
+ def create_owner_attribute_metadata_hash
218
+ local = local_owner_attribute_metadata_hash
219
+ superclass < Resource ? local.union(superclass.owner_attribute_metadata_hash) : local
220
+ end
221
+
222
+ # @return [<Attribute>] the owner attributes
223
+ def owner_attribute_metadata_enumerator
224
+ # Enumerate each owner Attribute, filtering out nil values.
225
+ @oa_enum ||= Enumerable::Enumerator.new(owner_attribute_metadata_hash, :each_value).filter
226
+ end
227
+
228
+ # Returns the attribute which references the owner. The owner attribute is the inverse
229
+ # of the given owner class inverse attribute, if it exists. Otherwise, the owner
230
+ # attribute is inferred by #{Inverse#detect_inverse_attribute}.
231
+
232
+ # @param klass (see #add_owner)
233
+ # @param [Symbol] inverse the owner -> dependent attribute
234
+ # @return [Symbol, nil] this class's owner attribute
235
+ def detect_owner_attribute(klass, inverse)
236
+ klass.attribute_metadata(inverse).inverse or detect_inverse_attribute(klass)
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,183 @@
1
+ require 'caruby/domain/metadata'
2
+ require 'caruby/resource'
3
+
4
+ module CaRuby
5
+ module Domain
6
+ # Importer extends a {Module} with Java class import support.
7
+ #
8
+ # A Java class is imported into JRuby on demand by referencing the class name.
9
+ # Import on demand is induced by a reference to the class, e.g., given the
10
+ # following domain resource module definition:
11
+ # module ClinicalTrials
12
+ # module Resource
13
+ # ...
14
+ # end
15
+ #
16
+ # CaRuby::Domain.extend_module(self, Resource, 'org.nci.ctms')
17
+ # then the first reference by name to +ClinicalTrials::Subject+
18
+ # imports the Java class +org.nci.ctms.Subject+ into the JRuby class wrapper
19
+ # +ClinicalTrials::Subject+. The +ClinicalTrials::Resource+ module is included
20
+ # in +ClinicalTrials::Subject+ and the Java property meta-data is introspected
21
+ # into {Attributes}.
22
+ module Importer
23
+ # Extends the given module with Java class meta-data import support.
24
+ #
25
+ # @param [Module] mod the module to extend
26
+ # @param [{Symbol => Object}] opts the extension options
27
+ # @option opts (see #configure)
28
+ def self.extend_module(mod, opts)
29
+ mod.extend(self).configure_importer(opts)
30
+ end
31
+
32
+ # Imports a Java class constant on demand. If the class does not already
33
+ # include this module's mixin, then the mixin is included in the class.
34
+ #
35
+ # @param [Symbol] symbol the missing constant
36
+ # @return [Class] the imported class
37
+ # @raise [NameError] if the symbol is not an importable Java class
38
+ def const_missing(symbol)
39
+ logger.debug { "Detecting whether #{symbol} is a #{@pkg} Java class..." }
40
+ # Append the symbol to the package to make the Java class name.
41
+ begin
42
+ klass = eval "Java::#{@pkg}.#{symbol}"
43
+ resource_import klass
44
+ rescue NameError
45
+ logger.debug { "#{symbol} is not recognized as a #{@pkg} Java class - #{$!}\n#{caller.qp}." }
46
+ super
47
+ end
48
+ logger.info(klass.pp_s)
49
+ klass
50
+ end
51
+
52
+ # Imports the given Java class and introspects the {Metadata}.
53
+ # The Java class is assumed to be defined in this module's package.
54
+ # This module's mixin is added to the class.
55
+ #
56
+ # @param [String] class_or_name the source directory
57
+ # @raise [NameError] if the symbol does not correspond to a Java class
58
+ # in this module's package
59
+ def resource_import(klass)
60
+ # Add the superclass metadata, if necessary.
61
+ sc = klass.superclass
62
+ unless sc < @mixin or klass.parent_module != sc.parent_module then
63
+ const_get(sc.name.demodulize)
64
+ end
65
+ java_import(klass)
66
+ ensure_metadata_introspected(klass)
67
+ klass
68
+ end
69
+
70
+ # @param [Class, String] class_or_name the class to import into this module
71
+ # @return [Class] the imported class
72
+ def java_import(class_or_name)
73
+ # JRuby 1.4.x does not support a class argument
74
+ begin
75
+ Class === class_or_name ? super(class_or_name.java_class.name) : super
76
+ rescue Exception
77
+ raise JavaImportError.new("#{class_or_name} is not a Java class - #{$!}")
78
+ end
79
+ end
80
+
81
+ # Configures this importer with the given options. This method is intended for use by the
82
+ # {#extend_module} method.
83
+ #
84
+ # @param [{Symbol => Object}] opts the extension options
85
+ # @option opts [String] :package the required Java package name
86
+ # @option opts [Module, Proc] :metadata the optional {Metadata} extension module or proc (default {Metadata})
87
+ # @option opts [Module] :mixin the optional mix-in module (default {Resource})
88
+ # @option opts [String] :directory the optional directory of source class definitions to load
89
+ def configure_importer(opts)
90
+ @pkg = opts[:package]
91
+ if @pkg.nil? then raise ArgumentError.new("Required domain package option not found") end
92
+ @metadata = opts[:metadata] || Metadata
93
+ @mixin = opts[:mixin] || Resource
94
+ @introspected = Set.new
95
+ dir = opts[:directory]
96
+ load_dir(dir) if dir
97
+ end
98
+
99
+ private
100
+
101
+ # Enables the given class {Metadata} if necessary.
102
+ #
103
+ # @param [Class] klass the class to enable
104
+ def ensure_metadata_introspected(klass)
105
+ add_metadata(klass) unless @introspected.include?(klass)
106
+ end
107
+
108
+ # Enables the given class meta-data.
109
+ #
110
+ # @param [Class] klass the class to enable
111
+ def add_metadata(klass)
112
+ # Mark the class as introspected. Do this first to preclude a recursive loop back
113
+ # into this method when the references are introspected in add_metadata.
114
+ @introspected << klass
115
+ # the package module
116
+ mod = klass.parent_module
117
+ # Add the superclass metadata, if necessary.
118
+ sc = klass.superclass
119
+ unless @introspected.include?(sc) or sc.parent_module != mod then
120
+ resource_import(sc)
121
+ end
122
+ # Include the mixin.
123
+ unless klass < @mixin then
124
+ mixin = @mixin
125
+ klass.class_eval { include mixin }
126
+ end
127
+ # Add the class metadata.
128
+ case @metadata
129
+ when Module then klass.extend(@metadata)
130
+ when Proc then @metadata.call(klass)
131
+ else raise MetadataError.new("#{self} metadata is neither a class nor a proc: #{@metadata.qp}")
132
+ end
133
+ klass.domain_module = self
134
+ # Add referenced domain class metadata as necessary.
135
+ klass.each_attribute_metadata do |attr_md|
136
+ ref = attr_md.type
137
+ if ref.nil? then raise MetadataError.new("#{self} #{attr_md} domain type is unknown.") end
138
+ unless @introspected.include?(ref) or ref.parent_module != mod then
139
+ logger.debug { "Adding #{qp} #{attr_md} reference #{ref.qp} metadata..." }
140
+ resource_import(ref)
141
+ end
142
+ end
143
+ end
144
+
145
+ # Loads the Ruby source files in the given directory.
146
+ #
147
+ # @param [String] dir the source directory
148
+ def load_dir(dir)
149
+ # Auto-load the files on demand.
150
+ syms = autoload_dir(dir)
151
+ # Load each file on demand.
152
+ syms.each do |sym|
153
+ klass = const_get(sym)
154
+ logger.info(klass.pp_s)
155
+ end
156
+ end
157
+
158
+ # Auto-loads the Ruby source files in the given directory.
159
+ #
160
+ # @param [String] dir the source directory
161
+ # @return [<Symbol>] the class constants that will be loaded
162
+ def autoload_dir(dir)
163
+ # the domain class definitions
164
+ srcs = Dir.glob(File.join(dir, "*.rb"))
165
+ # autoload the domain classes to ensure that definitions are picked up on demand in class hierarchy order
166
+ srcs.map do |file|
167
+ base_name = File.basename(file, ".rb")
168
+ sym = base_name.camelize.to_sym
169
+ # JRuby autoload of classes defined in a submodule of a Java wrapper class is not supported.
170
+ # However, this only occurs with the caTissue Specimen Pathology annotation class definitions,
171
+ # not the caTissue Participant or SCG annotations. TODO - confirm, isolate and report.
172
+ # Work-around is to require the files instead.
173
+ if name[/^Java::/] then
174
+ require file
175
+ else
176
+ autoload(sym, file)
177
+ end
178
+ sym
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,176 @@
1
+ require 'caruby/util/module'
2
+ require 'caruby/import/java'
3
+ require 'caruby/domain/java_attribute'
4
+
5
+ module CaRuby
6
+ module Domain
7
+ # Meta-data mix-in to infer attribute meta-data from Java properties.
8
+ module Introspection
9
+
10
+ protected
11
+
12
+ # @return [Boolean] whether this {Resource} class meta-data has been introspected
13
+ def introspected?
14
+ # initialization sets the attribute => metadata hash
15
+ not @attr_md_hash.nil?
16
+ end
17
+
18
+ # Defines the Java property attribute and standard attribute methods, e.g.
19
+ # +study_protocol+ and +studyProtocol+. A boolean attribute is provisioned
20
+ # with an additional reader alias, e.g. +available?+ for +is_available+.
21
+ #
22
+ # Each Java property attribute delegates to the Java property getter and setter.
23
+ # Each standard attribute delegates to the Java property attribute.
24
+ # Redefining these methods results in a call to the redefined method.
25
+ # This contrasts with a Ruby alias, where the alias remains bound to the
26
+ # original method body.
27
+ def introspect
28
+ # the module corresponding to the Java package of this class
29
+ mod = parent_module
30
+ # Set up the attribute data structures; delegates to Attributes.
31
+ init_attributes
32
+ logger.debug { "Introspecting #{qp} metadata..." }
33
+ # The Java properties defined by this class with both a read and a write method.
34
+ pds = java_properties(false)
35
+ # Define the standard Java attribute methods.
36
+ pds.each { |pd| define_java_attribute(pd) }
37
+ logger.debug { "Introspection of #{qp} metadata complete." }
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ # Defines the Java property attribute and standard attribute methods, e.g.
44
+ # +study_protocol+ and +studyProtocol+. A boolean attribute is provisioned
45
+ # with an additional reader alias, e.g. +available?+ for +is_available+.
46
+ #
47
+ # A standard attribute which differs from the property attribute delegates
48
+ # to the property attribute, e.g. +study_protocol+ delegates to +studyProtocol+
49
+ # rather than aliasing +setStudyProtocol+. Redefining these methods results
50
+ # in a call to the redefined method. This contrasts with a Ruby alias,
51
+ # where each attribute alias is bound to the respective property reader or
52
+ # writer.
53
+ def define_java_attribute(pd)
54
+ if transient?(pd) then
55
+ logger.debug { "Ignoring #{name.demodulize} transient property #{pd.name}." }
56
+ return
57
+ end
58
+ # the standard underscore lower-case attributes
59
+ attr = create_java_attribute(pd)
60
+ # delegate the standard attribute accessors to the property accessors
61
+ alias_attribute_property(attr, pd.name)
62
+ # add special wrappers
63
+ wrap_java_attribute(attr, pd)
64
+ # create Ruby alias for boolean, e.g. alias :empty? for :empty
65
+ if pd.property_type.name[/\w+$/].downcase == 'boolean' then
66
+ # strip leading is_, if any, before appending question mark
67
+ aliaz = attr.to_s[/^(is_)?(\w+)/, 2] << '?'
68
+ delegate_to_attribute(aliaz, attr)
69
+ end
70
+ end
71
+
72
+ # Adds a filter to the attribute access method for the property descriptor pd if it is a String or Date.
73
+ def wrap_java_attribute(attribute, pd)
74
+ if pd.property_type == Java::JavaLang::String.java_class then
75
+ wrap_java_string_attribute(attribute, pd)
76
+ elsif pd.property_type == Java::JavaUtil::Date.java_class then
77
+ wrap_java_date_attribute(attribute, pd)
78
+ end
79
+ end
80
+
81
+ # Adds a to_s filter to this Class's String property access methods.
82
+ def wrap_java_string_attribute(attribute, pd)
83
+ # filter the attribute writer
84
+ awtr = "#{attribute}=".to_sym
85
+ pwtr = pd.write_method.name.to_sym
86
+ define_method(awtr) do |value|
87
+ stdval = value.to_s unless value.nil_or_empty?
88
+ send(pwtr, stdval)
89
+ end
90
+ logger.debug { "Filtered #{qp} #{awtr} method with non-String -> String converter." }
91
+ end
92
+
93
+ # Adds a date parser filter to this Class's Date property access methods.
94
+ def wrap_java_date_attribute(attribute, pd)
95
+ # filter the attribute reader
96
+ prdr = pd.read_method.name.to_sym
97
+ define_method(attribute) do
98
+ value = send(prdr)
99
+ Java::JavaUtil::Date === value ? value.to_ruby_date : value
100
+ end
101
+
102
+ # filter the attribute writer
103
+ awtr = "#{attribute}=".to_sym
104
+ pwtr = pd.write_method.name.to_sym
105
+ define_method(awtr) do |value|
106
+ value = Java::JavaUtil::Date.from_ruby_date(value) if ::Date === value
107
+ send(pwtr, value)
108
+ end
109
+
110
+ logger.debug { "Filtered #{qp} #{attribute} and #{awtr} methods with Java Date <-> Ruby Date converter." }
111
+ end
112
+
113
+ # Aliases the methods _aliaz_ and _aliaz=_ to _property_ and _property=_, resp.,
114
+ # where _property_ is the Java property name for the attribute.
115
+ def alias_attribute_property(aliaz, attribute)
116
+ # strip the Java reader and writer is/get/set prefix and make a symbol
117
+ prdr, pwtr = attribute_metadata(attribute).property_accessors
118
+ alias_method(aliaz, prdr)
119
+ writer = "#{aliaz}=".to_sym
120
+ alias_method(writer, pwtr)
121
+ end
122
+
123
+ # Makes a standard attribute for the given property descriptor.
124
+ # Adds a camelized Java-like alias to the standard attribute.
125
+ #
126
+ # @quirk caTissue DE annotation collection attributes are often misnamed,
127
+ # e.g. +histologic_grade+ for a +HistologicGrade+ collection attribute.
128
+ # This is fixed by adding a pluralized alias, e.g. +histologic_grades+.
129
+ #
130
+ # @return a new attribute symbol created for the given PropertyDescriptor pd
131
+ def create_java_attribute(pd)
132
+ # make the attribute metadata
133
+ attr_md = JavaAttribute.new(pd, self)
134
+ add_attribute_metadata(attr_md)
135
+ # the property name is an alias for the standard attribute
136
+ std_attr = attr_md.to_sym
137
+ prop_attr = pd.name.to_sym
138
+ delegate_to_attribute(prop_attr, std_attr) unless prop_attr == std_attr
139
+
140
+ # alias a misnamed collection attribute, if necessary
141
+ if attr_md.collection? then
142
+ name = std_attr.to_s
143
+ if name.singularize == name then
144
+ aliaz = name.pluralize.to_sym
145
+ if aliaz != name then
146
+ logger.debug { "Adding annotation #{qp} alias #{aliaz} to the misnamed collection attribute #{std_attr}..." }
147
+ delegate_to_attribute(aliaz, std_attr)
148
+ end
149
+ end
150
+ end
151
+
152
+ std_attr
153
+ end
154
+
155
+ # Defines methods _aliaz_ and _aliaz=_ which calls the standard _attribute_ and
156
+ # _attribute=_ accessor methods, resp.
157
+ # Calling rather than aliasing the attribute accessor allows the aliaz accessor to
158
+ # reflect a change to the attribute accessor.
159
+ def delegate_to_attribute(aliaz, attribute)
160
+ if aliaz == attribute then raise MetadataError.new("Cannot delegate #{self} #{aliaz} to itself.") end
161
+ rdr, wtr = attribute_metadata(attribute).accessors
162
+ define_method(aliaz) { send(rdr) }
163
+ define_method("#{aliaz}=".to_sym) { |value| send(wtr, value) }
164
+ add_alias(aliaz, attribute)
165
+ end
166
+
167
+ # Makes a new synthetic attribute for each _method_ => _original_ hash entry.
168
+ #
169
+ # @param (see Class#offset_attr_accessor)
170
+ def offset_attribute(hash, offset=nil)
171
+ offset_attr_accessor(hash, offset)
172
+ hash.each { |attr, original| add_attribute(attr, attribute_metadata(original).type) }
173
+ end
174
+ end
175
+ end
176
+ end