caruby-core 1.4.9 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.md +48 -0
- data/lib/caruby/cli/command.rb +2 -1
- data/lib/caruby/csv/csv_mapper.rb +8 -8
- data/lib/caruby/database/persistable.rb +44 -65
- data/lib/caruby/database/persistence_service.rb +12 -9
- data/lib/caruby/database/persistifier.rb +14 -14
- data/lib/caruby/database/reader.rb +53 -51
- data/lib/caruby/database/search_template_builder.rb +9 -10
- data/lib/caruby/database/store_template_builder.rb +58 -58
- data/lib/caruby/database/writer.rb +96 -96
- data/lib/caruby/database.rb +19 -19
- data/lib/caruby/domain/attribute.rb +581 -0
- data/lib/caruby/domain/attributes.rb +615 -0
- data/lib/caruby/domain/dependency.rb +240 -0
- data/lib/caruby/domain/importer.rb +183 -0
- data/lib/caruby/domain/introspection.rb +176 -0
- data/lib/caruby/domain/inverse.rb +173 -0
- data/lib/caruby/domain/inversible.rb +1 -2
- data/lib/caruby/domain/java_attribute.rb +173 -0
- data/lib/caruby/domain/merge.rb +13 -10
- data/lib/caruby/domain/metadata.rb +141 -0
- data/lib/caruby/domain/mixin.rb +35 -0
- data/lib/caruby/domain/reference_visitor.rb +5 -3
- data/lib/caruby/domain.rb +340 -0
- data/lib/caruby/import/java.rb +29 -25
- data/lib/caruby/migration/migratable.rb +5 -5
- data/lib/caruby/migration/migrator.rb +19 -15
- data/lib/caruby/migration/resource_module.rb +1 -1
- data/lib/caruby/resource.rb +39 -30
- data/lib/caruby/util/collection.rb +94 -33
- data/lib/caruby/util/coordinate.rb +28 -2
- data/lib/caruby/util/log.rb +4 -4
- data/lib/caruby/util/module.rb +12 -28
- data/lib/caruby/util/partial_order.rb +9 -10
- data/lib/caruby/util/pretty_print.rb +46 -26
- data/lib/caruby/util/topological_sync_enumerator.rb +10 -4
- data/lib/caruby/util/transitive_closure.rb +2 -2
- data/lib/caruby/util/visitor.rb +1 -1
- data/lib/caruby/version.rb +1 -1
- data/test/lib/caruby/database/persistable_test.rb +1 -1
- data/test/lib/caruby/domain/domain_test.rb +14 -28
- data/test/lib/caruby/domain/inversible_test.rb +1 -1
- data/test/lib/caruby/import/java_test.rb +5 -0
- data/test/lib/caruby/migration/test_case.rb +0 -1
- data/test/lib/caruby/test_case.rb +9 -10
- data/test/lib/caruby/util/collection_test.rb +23 -5
- data/test/lib/caruby/util/module_test.rb +10 -14
- data/test/lib/caruby/util/partial_order_test.rb +16 -15
- data/test/lib/caruby/util/visitor_test.rb +1 -1
- data/test/lib/examples/galena/clinical_trials/migration/test_case.rb +1 -1
- metadata +16 -15
- data/History.txt +0 -44
- data/lib/caruby/domain/attribute_metadata.rb +0 -551
- data/lib/caruby/domain/java_attribute_metadata.rb +0 -183
- data/lib/caruby/domain/resource_attributes.rb +0 -565
- data/lib/caruby/domain/resource_dependency.rb +0 -217
- data/lib/caruby/domain/resource_introspection.rb +0 -160
- data/lib/caruby/domain/resource_inverse.rb +0 -151
- data/lib/caruby/domain/resource_metadata.rb +0 -155
- data/lib/caruby/domain/resource_module.rb +0 -370
- data/lib/caruby/yard/resource_metadata_handler.rb +0 -8
@@ -0,0 +1,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
|