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