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