caruby-core 1.4.7 → 1.4.9

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 (48) hide show
  1. data/History.txt +11 -0
  2. data/README.md +1 -1
  3. data/lib/caruby/cli/command.rb +27 -3
  4. data/lib/caruby/csv/csv_mapper.rb +2 -0
  5. data/lib/caruby/csv/csvio.rb +187 -169
  6. data/lib/caruby/database.rb +33 -16
  7. data/lib/caruby/database/lazy_loader.rb +23 -23
  8. data/lib/caruby/database/persistable.rb +32 -18
  9. data/lib/caruby/database/persistence_service.rb +20 -7
  10. data/lib/caruby/database/reader.rb +22 -21
  11. data/lib/caruby/database/search_template_builder.rb +7 -9
  12. data/lib/caruby/database/sql_executor.rb +52 -27
  13. data/lib/caruby/database/store_template_builder.rb +18 -13
  14. data/lib/caruby/database/writer.rb +107 -44
  15. data/lib/caruby/domain/attribute_metadata.rb +35 -25
  16. data/lib/caruby/domain/java_attribute_metadata.rb +43 -20
  17. data/lib/caruby/domain/merge.rb +9 -5
  18. data/lib/caruby/domain/reference_visitor.rb +4 -3
  19. data/lib/caruby/domain/resource_attributes.rb +52 -12
  20. data/lib/caruby/domain/resource_dependency.rb +129 -42
  21. data/lib/caruby/domain/resource_introspection.rb +1 -1
  22. data/lib/caruby/domain/resource_inverse.rb +20 -3
  23. data/lib/caruby/domain/resource_metadata.rb +20 -4
  24. data/lib/caruby/domain/resource_module.rb +190 -124
  25. data/lib/caruby/import/java.rb +39 -19
  26. data/lib/caruby/migration/migratable.rb +31 -6
  27. data/lib/caruby/migration/migrator.rb +126 -40
  28. data/lib/caruby/migration/uniquify.rb +0 -1
  29. data/lib/caruby/resource.rb +28 -5
  30. data/lib/caruby/util/attribute_path.rb +0 -2
  31. data/lib/caruby/util/class.rb +8 -5
  32. data/lib/caruby/util/collection.rb +5 -3
  33. data/lib/caruby/util/domain_extent.rb +0 -3
  34. data/lib/caruby/util/options.rb +10 -9
  35. data/lib/caruby/util/person.rb +41 -12
  36. data/lib/caruby/util/pretty_print.rb +1 -1
  37. data/lib/caruby/util/validation.rb +0 -28
  38. data/lib/caruby/version.rb +1 -1
  39. data/test/lib/caruby/import/java_test.rb +26 -9
  40. data/test/lib/caruby/migration/test_case.rb +103 -0
  41. data/test/lib/caruby/test_case.rb +231 -0
  42. data/test/lib/caruby/util/class_test.rb +2 -2
  43. data/test/lib/caruby/util/visitor_test.rb +3 -2
  44. data/test/lib/examples/galena/clinical_trials/migration/participant_test.rb +28 -0
  45. data/test/lib/examples/galena/clinical_trials/migration/test_case.rb +40 -0
  46. metadata +195 -170
  47. data/lib/caruby/domain/attribute_initializer.rb +0 -16
  48. data/test/lib/caruby/util/validation_test.rb +0 -14
@@ -1,3 +1,5 @@
1
+ require 'caruby/util/validation'
2
+
1
3
  module CaRuby
2
4
  # ResourceMetadata mix-in to capture Resource dependency.
3
5
  module ResourceDependency
@@ -25,24 +27,49 @@ module CaRuby
25
27
  # * _inverse_ is the owner inverse attribute defined in the dependent class
26
28
  #
27
29
  # @param [Symbol] attribute the dependent to add
28
- # @param [<Symbol>] the attribute qualifier flags
30
+ # @param [<Symbol>] flags the attribute qualifier flags
29
31
  def add_dependent_attribute(attribute, *flags)
30
32
  attr_md = attribute_metadata(attribute)
31
33
  flags << :dependent unless flags.include?(:dependent)
32
34
  attr_md.qualify(*flags)
33
35
  inverse = attr_md.inverse
34
36
  inv_type = attr_md.type
35
- # example: Parent.add_dependent_attribute(:children) with inverse :parent calls
36
- # Child.add_owner(Parent, :children, :parent)
37
+ # example: Parent.add_dependent_attribute(:children) with inverse :parent calls the following:
38
+ # Child.add_owner(Parent, :children, :parent)
37
39
  inv_type.add_owner(self, attribute, inverse)
38
40
  end
39
-
40
- # Returns whether this metadata's subject class depends on an owner.
41
+
42
+ # Makes a new owner attribute. The attribute name is the lower-case demodulized
43
+ # owner class name. The owner class must reference this class via the given
44
+ # inverse dependent attribute.
45
+ #
46
+ # @param klass (see #detect_owner_attribute)
47
+ # @param [Symbol] the owner -> dependent inverse attribute
48
+ # @return [Symbol] this class's new owner attribute
49
+ # @raise [ArgumentError] if the inverse is nil
50
+ def create_owner_attribute(klass, inverse)
51
+ if inverse.nil? then
52
+ raise ArgumentError.new("Cannot create a #{qp} owner attribute to #{klass} without a dependent attribute to this class.")
53
+ end
54
+ attr = klass.name.demodulize.underscore.to_sym
55
+ attr_accessor(attr)
56
+ add_attribute(attr, klass)
57
+ attribute_metadata(attr).inverse = inverse
58
+ logger.debug { "Created #{qp} owner attribute #{attr} with inverse #{klass.qp}.#{inverse}." }
59
+ attr
60
+ end
61
+
62
+ # @return [Boolean] whether this class depends on an owner
41
63
  def dependent?
42
64
  not owners.empty?
43
65
  end
66
+
67
+ # @return [Boolean] whether this class has an owner which cascades save operations to this dependent
68
+ def cascaded_dependent?
69
+ owner_attribute_metadata_enumerator.any? { |attr_md| attr_md.cascaded? }
70
+ end
44
71
 
45
- # Returns whether this metadata's subject class depends the given other class.
72
+ # @return [Boolean] whether this class depends the given other class
46
73
  def depends_on?(other)
47
74
  owners.detect { |owner| owner === other }
48
75
  end
@@ -58,73 +85,133 @@ module CaRuby
58
85
  # @return [Symbol, nil] the sole owner attribute of this class, or nil if there
59
86
  # is not exactly one owner
60
87
  def owner_attribute
61
- if @local_owner_attr_hash then
62
- # the sole attribute in the owner class => attribute hash
63
- @local_owner_attr_hash.each_value { |attr| return attr } if @local_owner_attr_hash.size == 1
64
- elsif superclass < Resource
65
- # delegate to superclass
66
- superclass.owner_attribute
88
+ attr_md = owner_attribute_metadata_enumerator.first || return
89
+ # There is at most one non-nil value in the owner class => AttributeMetadata hash.
90
+ # If there is such a value, then return the attribute symbol.
91
+ if owner_attribute_metadata_enumerator.size == 1 then
92
+ attr_md.to_sym
67
93
  end
68
94
  end
69
95
 
70
- # Returns this Resource class's owner types.
96
+ # @return [<Symbol>] this class's owner attributes
71
97
  def owner_attributes
72
- if @local_owner_attr_hash then
73
- @local_owner_attrs ||= Enumerable::Enumerator.new(@local_owner_attr_hash, :each_value).filter
74
- elsif superclass < Resource
75
- superclass.owner_attributes
76
- else
77
- Array::EMPTY_ARRAY
78
- end
98
+ owner_attribute_metadata_enumerator.transform { |attr_md| attr_md.to_sym }
79
99
  end
80
100
 
81
- # Returns this Resource class's dependent types.
101
+ # @return [<Class>] this class's dependent types
82
102
  def dependents
83
103
  dependent_attributes.wrap { |attr| attr.type }
84
104
  end
85
105
 
86
- # Returns this Resource class's owner types.
106
+ # @return [<Class>] this class's owner types
87
107
  def owners
88
- if @local_owner_attr_hash then
89
- @local_owners ||= Enumerable::Enumerator.new(@local_owner_attr_hash, :each_key)
90
- elsif superclass < Resource
91
- superclass.owners
92
- else
93
- Array::EMPTY_ARRAY
94
- end
108
+ @owners ||= Enumerable::Enumerator.new(owner_attribute_metadata_hash, :each_key)
95
109
  end
96
110
 
97
111
  protected
98
112
 
99
113
  # Adds the given owner class to this dependent class.
100
114
  # This method must be called before any dependent attribute is accessed.
115
+ # If the attribute is given, then the attribute inverse is set.
116
+ # Otherwise, if there is not already an owner attribute, then a new owner attribute is created.
117
+ # The name of the new attribute is the lower-case demodulized owner class name.
101
118
  #
102
119
  # @param [Class] the owner class
103
- # @param [Symbol, nil] inverse the owner -> dependent attribute
120
+ # @param [Symbol] inverse the owner -> dependent attribute
104
121
  # @param [Symbol, nil] attribute the dependent -> owner attribute, if known
105
- # @raise [ValidationError] if there is no owner -> dependent inverse attribute
106
- # @raise [MetadataError] if this method is called after a dependent attribute has been accessed
122
+ # @raise [ValidationError] if the inverse is nil
107
123
  def add_owner(klass, inverse, attribute=nil)
124
+ if inverse.nil? then raise ValidationError.new("Owner #{klass.qp} missing dependent attribute for dependent #{qp}") end
108
125
  logger.debug { "Adding #{qp} owner #{klass.qp}#{' attribute ' + attribute.to_s if attribute}#{' inverse ' + inverse.to_s if inverse}..." }
109
126
  if @owner_attr_hash then
110
127
  raise MetadataError.new("Can't add #{qp} owner #{klass.qp} after dependencies have been accessed")
111
128
  end
112
- @local_owner_attr_hash ||= {}
113
- @local_owner_attr_hash[klass] = attribute ||= detect_inverse_attribute(klass)
129
+
130
+ # detect the owner attribute, if necessary
131
+ attribute ||= detect_owner_attribute(klass, inverse)
132
+ attr_md = attribute_metadata(attribute) if attribute
133
+ # Add the owner class => attribute entry.
134
+ # The attribute is nil if the dependency is unidirectional, i.e. there is an owner class which
135
+ # references this class via a dependency attribute but there is no inverse owner attribute.
136
+ local_owner_attribute_metadata_hash[klass] = attr_md
137
+ # If the dependency is unidirectional, then our job is done.
138
+ return if attribute.nil?
114
139
 
115
- # set the inverse
116
- if attribute then
117
- if inverse.nil? then raise ValidationError.new("Owner #{klass.qp} missing dependent attribute for dependent #{qp}") end
140
+ # set the inverse if necessary
141
+ unless attr_md.inverse then
118
142
  set_attribute_inverse(attribute, inverse)
119
- attribute_metadata(attribute).qualify(:owner)
143
+ end
144
+ # set the owner flag if necessary
145
+ unless attr_md.owner? then attr_md.qualify(:owner) end
146
+ # Redefine the writer method to warn when changing the owner
147
+ rdr, wtr = attr_md.accessors
148
+ logger.debug { "Injecting owner change warning into #{qp}.#{attribute} writer method #{wtr}..." }
149
+ redefine_method(wtr) do |old_wtr|
150
+ lambda do |ref|
151
+ prev = send(rdr)
152
+ if prev and prev != ref then
153
+ if ref.nil? then
154
+ logger.warn("Unsetting the #{self} owner #{attribute} #{prev}.")
155
+ elsif ref.identifier != prev.identifier then
156
+ logger.warn("Resetting the #{self} owner #{attribute} from #{prev} to #{ref}.")
157
+ end
158
+ end
159
+ send(old_wtr, ref)
160
+ end
161
+ end
162
+ end
163
+
164
+ # Adds the given attribute as an owner. This method is called when a new attribute is added that
165
+ # references an existing owner.
166
+ #
167
+ # @param [Symbol] attribute the owner attribute
168
+ def add_owner_attribute(attribute)
169
+ attr_md = attribute_metadata(attribute)
170
+ otype = attr_md.type
171
+ hash = local_owner_attribute_metadata_hash
172
+ if hash.include?(otype) then
173
+ oattr = hash[otype]
174
+ unless oattr.nil? then
175
+ raise MetadataError.new("Cannot set #{qp} owner attribute to #{attribute} since it is already set to #{oattr}")
176
+ end
177
+ hash[otype] = attr_md
120
178
  else
121
- logger.debug { "No #{qp} owner attribute detected for #{klass.qp}." }
179
+ add_owner(otype, attr_md.inverse, attribute)
122
180
  end
123
181
  end
124
182
 
125
- # @return [{Class => Symbol}] this Resource class's owner type => attribute hash
126
- def owner_attribute_hash
127
- @local_owner_attr_hash or (superclass.owner_attribute_hash if superclass < Resource) or Hash::EMPTY_HASH
183
+ # @return [{Class => AttributeMetadata}] this class's owner type => attribute hash
184
+ def owner_attribute_metadata_hash
185
+ @oa_hash ||= create_owner_attribute_metadata_hash
186
+ end
187
+
188
+ private
189
+
190
+ def local_owner_attribute_metadata_hash
191
+ @local_oa_hash ||= {}
192
+ end
193
+
194
+ # @return [{Class => AttributeMetadata}] a new owner type => attribute hash
195
+ def create_owner_attribute_metadata_hash
196
+ local = local_owner_attribute_metadata_hash
197
+ superclass < Resource ? local.union(superclass.owner_attribute_metadata_hash) : local
198
+ end
199
+
200
+ # @return [<AttributeMetadata>] the owner attributes
201
+ def owner_attribute_metadata_enumerator
202
+ # Enumerate each owner AttributeMetadata, filtering out nil values.
203
+ @oa_enum ||= Enumerable::Enumerator.new(owner_attribute_metadata_hash, :each_value).filter
204
+ end
205
+
206
+ # Returns the attribute which references the owner. The owner attribute is the inverse
207
+ # of the given owner class inverse attribute, if it exists. Otherwise, the owner
208
+ # attribute is inferred by #{ResourceInverse#detect_inverse_attribute}.
209
+
210
+ # @param klass (see #add_owner)
211
+ # @param [Symbol] inverse the owner -> dependent attribute
212
+ # @return [Symbol, nil] this class's owner attribute
213
+ def detect_owner_attribute(klass, inverse)
214
+ klass.attribute_metadata(inverse).inverse or detect_inverse_attribute(klass)
128
215
  end
129
216
  end
130
217
  end
@@ -96,7 +96,7 @@ module CaRuby
96
96
  logger.debug { "Filtered #{qp} #{attribute} and #{awtr} methods with Java Date <-> Ruby Date converter." }
97
97
  end
98
98
 
99
- # Aliases the methods aliaz and _aliaz= to _property_ and _property=_, resp.,
99
+ # Aliases the methods _aliaz_ and _aliaz=_ to _property_ and _property=_, resp.,
100
100
  # where _property_ is the Java property name for the attribute.
101
101
  def alias_attribute_property(aliaz, attribute)
102
102
  # strip the Java reader and writer is/get/set prefix and make a symbol
@@ -7,6 +7,21 @@ module CaRuby
7
7
 
8
8
  protected
9
9
 
10
+ # Infers the inverse of the given attribute declared by this class. A domain attribute is
11
+ # recognized as an inverse according to the {ResourceInverse#detect_inverse_attribute}
12
+ # criterion.
13
+ #
14
+ # @param [AttributeMetadata] attr_md the attribute to check
15
+ def infer_attribute_inverse(attr_md)
16
+ inv = attr_md.type.detect_inverse_attribute(self)
17
+ if inv then
18
+ logger.debug { "Detected #{qp} #{attr_md} inverse #{inv.qp}." }
19
+ set_attribute_inverse(attr_md.to_sym, inv)
20
+ else
21
+ logger.debug { "No inverse detected for #{qp} #{attr_md}." }
22
+ end
23
+ end
24
+
10
25
  # Sets the given bi-directional association attribute's inverse.
11
26
  #
12
27
  # @param [Symbol] attribute the subject attribute
@@ -29,13 +44,15 @@ module CaRuby
29
44
  raise TypeError.new("Cannot set #{qp}.#{attribute} inverse to #{attr_md.type.qp}.#{attribute} with incompatible type #{inv_md.type.qp}")
30
45
  end
31
46
 
32
- # if the attribute is defined by this class, then set the inverse in the attribute metadata.
33
- # otherwise, make a new attribute metadata specialized for this class.
47
+ # if the attribute is not declared by this class, then make a new attribute
48
+ # metadata specialized for this class.
34
49
  unless attr_md.declarer == self then
35
50
  attr_md = attr_md.dup
36
51
  attr_md.declarer = self
37
52
  add_attribute_metadata(attribute, inverse)
53
+ logger.debug { "Copied #{attr_md.declarer}.#{attribute} to #{qp} with restricted inverse return type #{qp}." }
38
54
  end
55
+ # Set the inverse in the attribute metadata.
39
56
  attr_md.inverse = inverse
40
57
 
41
58
  # If attribute is the one side of a 1:M or non-reflexive 1:1 relation, then add the inverse updater.
@@ -57,7 +74,7 @@ module CaRuby
57
74
  # or nil if no owner attribute was detected
58
75
  def detect_inverse_attribute(klass)
59
76
  # the candidate attributes which return the referencing type
60
- candidates = domain_attributes.compose { |attr_md| klass <= attr_md.type }.to_a
77
+ candidates = domain_attributes.compose { |attr_md| klass <= attr_md.type }
61
78
  attr = detect_inverse_attribute_from_candidates(klass, candidates)
62
79
  if attr then
63
80
  logger.debug { "#{qp} #{klass.qp} inverse attribute is #{attr}." }
@@ -15,6 +15,23 @@ module CaRuby
15
15
  include ResourceIntrospection, ResourceInverse, ResourceDependency, ResourceAttributes
16
16
 
17
17
  attr_reader :domain_module
18
+
19
+ # JRuby 1.6 alert - if a concrete class implements a constructor, then calling super in an
20
+ # included module initialize method results in a recursive call back into that initialize.
21
+ # The work-around is to redefine each domain class new method to call the initalize_content
22
+ # method instead. All Resource domain classes or included modules must not override
23
+ # initialize. They can implement initialize_content instead.
24
+ def self.extended(klass)
25
+ klass.class_eval do
26
+ class << self
27
+ def new(opts=nil)
28
+ obj = super()
29
+ obj.merge_attributes(opts) if opts
30
+ obj
31
+ end
32
+ end
33
+ end
34
+ end
18
35
 
19
36
  # Associates meta-data with a Resource class. When a Resource class is added to a ResourceModule,
20
37
  # the ResourceModule extends the Resource class with ResourceMetadata and calls this
@@ -59,15 +76,13 @@ module CaRuby
59
76
 
60
77
  # Prints this classifier's content to the log.
61
78
  def pretty_print(q)
62
-
63
-
64
- # KLUDGE - if not inited then bail
79
+ # KLUDGE - if not inited then bail.
80
+ # TODO - isolate when this occurs.
65
81
  if @attr_md_hash.nil? then
66
82
  q.text(qp)
67
83
  return
68
84
  end
69
85
 
70
-
71
86
  # the Java property descriptors
72
87
  property_descriptors = java_attributes.wrap { |attr| attribute_metadata(attr).property_descriptor }
73
88
  # build a map of relevant display label => attributes
@@ -99,6 +114,7 @@ module CaRuby
99
114
  "dependent attributes" => dependents_printer,
100
115
  "default values" => defaults
101
116
  }.delete_if { |key, value| value.nil_or_empty? }
117
+
102
118
  # one indented line per entry, all but the last line ending in a comma
103
119
  content = map.map { |label, value| " #{label}=>#{format_print_value(value)}" }.join(",\n")
104
120
  # print the content to the log
@@ -11,9 +11,11 @@ module CaRuby
11
11
  [:password, "--password PSWD", "the application login password"],
12
12
  [:host, "--host HOST", "the application host name"],
13
13
  [:port, "--port PORT", "the application port number"],
14
+ [:classpath, "--classpath PATH", "the application client classpath"],
14
15
  [:database_host, "--database_host HOST", "the database host name"],
15
16
  [:database_type, "--database_type TYPE", "the database type (mysql or oracle)"],
16
- [:database_driver, "--database_driver DRIVER", "the database driver"],
17
+ [:database_driver, "--database_driver DRIVER", "the database driver string"],
18
+ [:database_driver_class, "--database_driver_class CLASS", "the database driver class name"],
17
19
  [:database_port, "--database_port PORT", Integer, "the database port number"],
18
20
  [:database, "--database NAME", "the database name"],
19
21
  [:database_user, "--database_user USER", "the database login user"],
@@ -31,120 +33,98 @@ module CaRuby
31
33
  # The properties are read from a property file. See {Properties} for
32
34
  # more information.
33
35
  #
34
- # A Java class is imported into Ruby either by directly calling the
35
- # extended module {#java_import} method or on demand.
36
+ # A Java class is imported into Ruby either by directly calling the extended
37
+ # module {#resource_import} method or on demand by referencing the class name.
36
38
  # Import on demand is induced by a reference to the class, e.g.:
37
39
  # module ClinicalTrials
38
- # extend Importable
40
+ # extend CaRuby::ResourceModule
39
41
  #
40
- # def java_package
41
- # 'org.nci.ctms'
42
- # end
42
+ # @java_package = 'org.nci.ctms'
43
43
  # ...
44
44
  # enables references by name to a +ClinicalTrials+ Ruby class wrapper of a
45
45
  # +org.nci.ctms+ Java class without an import statement, e.g.:
46
46
  # ClinicalTrials::Participant.new
47
47
  # without defining the +Participant+ Ruby class.
48
48
  module ResourceModule
49
- # Adds the given class to this ResourceModule. The class is extended with ResourceMetadata methods.
50
- #
51
- # @param [Class] the {Resource} class to add
52
- def add_class(klass)
53
- @rsc_classes ||= Set.new
54
- # add superclass if necessary
55
- unless @rsc_classes.include?(klass.superclass) or klass.superclass == Java::JavaLang::Object then
56
- # the domain module includes the superclass on demand
57
- const_get(klass.superclass.name[/\w+$/].to_sym)
58
- end
59
- ResourceMetadata.extend_class(klass, self)
60
- @rsc_classes << klass
49
+ # Loads the {#access_properties} and adds the path property items to the Java classpath.
50
+ # @param [Module] mod the module to extend
51
+ def self.extended(mod)
52
+ super
53
+ mod.ensure_classpath_defined
61
54
  end
62
55
 
63
- # @return [Module] the resource mix-in module (default {Resouce})
64
- def mixin
65
- @mixin || Resource
66
- end
67
-
68
- # @return [{Symbol => Object}] the caBIG application access properties
69
- # @see #load_access_properties
70
- def access_properties
71
- @rsc_props ||= load_access_properties
72
- end
73
-
74
- # Loads the application start-up properties in the given file path.
75
- # The default file path is a period followed by the lower-case application name in the home directory,
76
- # e.g. +~/.clincaltrials+.
56
+ # Loads the application start-up properties on demand. The properties are defined in the properties
57
+ # file or as environment variables.
58
+ # The properties file path is a period followed by the lower-case application name in the home directory,
59
+ # e.g. +~/.clincaltrials+ for the +ClinicalTrials+ application.
77
60
  #
78
61
  # The property file format is a series of property definitions in the form _property_: _value_.
79
62
  # The supported properties include the following:
80
- # * +path+ - the application client Java directories
81
- # * +user+ - the application service login
63
+ # * +host+ - the application server host (default +localhost+)
64
+ # * +port+ - the application server port (default +8080+)
65
+ # * +user+ - the application server login
82
66
  # * +password+ - the application service password
67
+ # * +path+ or +classpath+ - the application client Java directories
83
68
  # * +database+ - the application database name
84
69
  # * +database_user+ - the application database connection userid
85
70
  # * +database_password+ - the application database connection password
86
- # * :database_host - the application database connection host (default +localhost+)
71
+ # * +database_host+ - the application database connection host (default +localhost+)
87
72
  # * +database_type+ - the application database type, + mysql+ or +oracle+ (default +mysql+)
88
73
  # * +database_driver+ - the application database connection driver (default is the database type default)
89
- # * +database_port+ - the application database connection port
74
+ # * +database_port+ - the application database connection port (default is the database type default)
90
75
  #
91
76
  # The +path+ value is one or more directories separated by a semi-colon(;) or colon (:)
92
77
  # Each path directory and all jar files within the directory are added to the caRuby execution
93
78
  # Java classpath.
94
79
  #
95
- # @param [String, nil] file the property file, or nil for the default location
96
- # @return [{Symbol => Object}] the loaded caBIG application access properties
97
- def load_access_properties(file=nil)
98
- # If a file was specified, then it must exist.
99
- if file and not File.exists?(file) then
100
- raise ArgumentError.new("Application access properties file does not exist: #{file}")
101
- end
102
- # the access properties
103
- props ||= {}
104
- # If no file was specified, then try the default.
105
- # If the default does not exist, then use the empty properties hash.
106
- # It is not an error to omit access properties, since the application domain classes
107
- # can still be used but not queried or saved.
108
- file ||= default_properties_file || return
109
-
110
- logger.info("Loading application properties from #{file}...")
111
- File.open(file).map do |line|
112
- # match the tolerant property definition
113
- match = PROP_DEF_REGEX.match(line.chomp) || next
114
- # the property [name, value] tokens
115
- tokens = match.captures
116
- name = tokens.first.to_sym
117
- value = tokens.last
118
- # capture the property
119
- props[name] = value
120
- end
121
- # Look for environment overrides preceded by the uppercase module name, e.g. CATISSUE
122
- # for the CaTissue module.
123
- env_prefix = name[/\w+$/].upcase
124
- ACCESS_OPTS.each do |spec|
125
- # the access option symbol is the first specification item
126
- opt = spec[0]
127
- # the envvar, e.g. CATISSUE_USER
128
- ev = "#{env_prefix}_#{opt.to_s.upcase}"
129
- # the envvar value
130
- value = ENV[ev] || next
131
- # override the file property with the envar value
132
- props[opt] = value
133
- logger.info("Set application property #{opt} from environment variable #{ev}.")
80
+ # Each property has an environment variable counterpart given by
81
+ #
82
+ # @return [{Symbol => Object}] the caBIG application access properties
83
+ def access_properties
84
+ @rsc_props ||= load_access_properties
85
+ end
86
+
87
+ # Ensures that the application client classpath is defined. The classpath is defined
88
+ # in the {#access_properties}. This method is called when a module extends this
89
+ # ResourceModule, before any application Java domain class is imported into JRuby.
90
+ def ensure_classpath_defined
91
+ # Loading the access properties on demand sets the classpath.
92
+ access_properties
93
+ end
94
+
95
+ # @return [Module] the resource mix-in module (default {Resouce})
96
+ def mixin
97
+ @mixin || Resource
98
+ end
99
+
100
+ # Adds the given class to this ResourceModule. The class is extended with ResourceMetadata methods.
101
+ #
102
+ # @param [Class] the {Resource} class to add
103
+ def add_class(klass)
104
+ logger.debug { "Adding #{klass.java_class.name} to #{qp}..." }
105
+ @rsc_classes ||= Set.new
106
+ # add superclass if necessary
107
+ sc = klass.superclass
108
+ unless @rsc_classes.include?(sc) then
109
+ # the domain module includes the superclass on demand
110
+ sc_pkg, sc_sym = Java.split_class_name(sc)
111
+ if const_defined?(sc_sym) or sc_pkg == @java_package then
112
+ const_get(sc_sym)
113
+ else
114
+ mod = mixin
115
+ klass.class_eval { include mod }
116
+ end
134
117
  end
135
-
136
- # load the Java application jar path
137
- path_ev = "#{env_prefix}_PATH"
138
- path = ENV[path_ev] || props[:path]
139
- Java.add_path(path) if path
140
-
141
- props
118
+ ResourceMetadata.extend_class(klass, self)
119
+ @rsc_classes << klass
120
+ class_added(klass)
121
+ logger.debug { "#{klass.java_class.name} added to #{qp}." }
142
122
  end
143
123
 
144
- # Loads the Ruby source files in the given directory.
124
+ # Auto-loads the Ruby source files in the given directory.
125
+ #
126
+ # @param [String] dir the source directory
145
127
  def load_dir(dir)
146
- # load the properties on demand
147
- load_access_properties if @rsc_props.nil?
148
128
  # the domain class definitions
149
129
  sources = Dir.glob(File.join(dir, "*.rb"))
150
130
 
@@ -171,21 +151,34 @@ module CaRuby
171
151
  end
172
152
  end
173
153
 
174
- def java_import(klass)
154
+ # @param [Class, String] class_or_name the class to import into this module
155
+ # @return [Class] the imported class
156
+ def java_import(class_or_name)
175
157
  # JRuby 1.4.x does not support a class argument
176
- Class === klass ? super(klass.java_class.name) : super
158
+ Class === class_or_name ? super(class_or_name.java_class.name) : super
159
+ end
160
+
161
+ # @param [Class, String] class_or_name the class to import into this module
162
+ # @return [Class] the imported {Resource} class
163
+ def resource_import(class_or_name)
164
+ klass = java_import(class_or_name)
165
+ mod = mixin
166
+ klass.instance_eval { include mod }
167
+ add_class(klass)
168
+ klass
177
169
  end
178
170
 
179
- # Extends the mod module with Java class support. See the class documentation for details.
171
+ # Imports a class constant on demand. See the class documentation for details.
180
172
  #
181
173
  # @param [Symbol] symbol the missing constant
182
174
  def const_missing(symbol)
183
175
  autoload?(symbol) ? super : import_domain_class(symbol)
184
176
  end
185
177
 
186
- # Returns the domain class for class_name, or nil if none in this module.
187
- def domain_type_with_name(class_name)
188
- pkg, base = split_class_name(class_name)
178
+ # @param [String] the class name to check
179
+ # @eturn [Class, nil] the domain class for the class name, or nil if none in this module
180
+ def domain_type_with_name(name)
181
+ pkg, base = Java.split_class_name(name)
189
182
  return unless pkg.nil? or pkg == @java_package
190
183
  begin
191
184
  type = const_get(base)
@@ -200,6 +193,77 @@ module CaRuby
200
193
 
201
194
  private
202
195
 
196
+ # Callback invoked after the given domain class is added to this domain module.
197
+ #
198
+ # @param [Class] klass the class that was added
199
+ def class_added(klass); end
200
+
201
+ # Loads the application start-up properties in the given file path.
202
+ #
203
+ # @return (see #access_properties)
204
+ def load_access_properties
205
+ # the properties file
206
+ file = default_properties_file
207
+ # the access properties
208
+ props = file && File.exists?(file) ? load_properties_file(file) : {}
209
+ # Look for environment overrides preceded by the uppercase module name,
210
+ # e.g. CATISSUE_USER for the CaTissue module.
211
+ load_environment_properties(props)
212
+
213
+ # load the Java application jar path
214
+ path = props[:classpath] || props[:path]
215
+ if path then
216
+ logger.info("Defining application classpath #{path}...")
217
+ Java.add_path(path)
218
+ end
219
+
220
+ props
221
+ end
222
+
223
+ def load_properties_file(file)
224
+ props = {}
225
+ logger.info("Loading application properties from #{file}...")
226
+ File.open(file).map do |line|
227
+ # match the tolerant property definition
228
+ match = PROP_DEF_REGEX.match(line.chomp) || next
229
+ # the property [name, value] tokens
230
+ tokens = match.captures
231
+ pname = tokens.first.to_sym
232
+ # path is deprecated
233
+ name = pname == :path ? :classpath : pname
234
+ value = tokens.last
235
+ # capture the property
236
+ props[name] = value
237
+ end
238
+ props
239
+ end
240
+
241
+ def load_environment_properties(props)
242
+ ACCESS_OPTS.each do |spec|
243
+ # the access option symbol is the first specification item
244
+ opt = spec[0]
245
+ # the envvar value
246
+ value = environment_property(opt) || next
247
+ # override the file property with the envar value
248
+ props[opt] = value
249
+ logger.info("Set application property #{opt} from environment variable #{ev}.")
250
+ end
251
+ end
252
+
253
+ # @param [Symbol] opt the property symbol, e.g. :user
254
+ # @return [String, nil] the value of the corresponding environment variable, e.g. +CATISSUE_USER+
255
+ def environment_property(opt)
256
+ @env_prefix ||= name.gsub('::', '_').upcase
257
+ ev = "#{@env_prefix}_#{opt.to_s.upcase}"
258
+ value = ENV[ev]
259
+ # If no classpath envvar, then try the deprecated path envvar.
260
+ if value.nil? and opt == :classpath then
261
+ environment_property(:path)
262
+ else
263
+ value
264
+ end
265
+ end
266
+
203
267
  # Imports the domain Java class with specified class name_or_sym.
204
268
  # This method enables the domain class extensions for storing and retrieving domain objects.
205
269
  # The class is added to this module.
@@ -222,14 +286,10 @@ module CaRuby
222
286
  #
223
287
  # The optional aliases argument consists of additional alias => standard attribute associations.
224
288
  # The optional owner_attr argument is a non-Java annotation owner attribute.
225
- def import_domain_class(name_or_sym)
226
- name = name_or_sym.to_s
227
- if name.include?('.') then
228
- symbol = name[/[A-Z]\w*$/].to_sym
229
- else
230
- symbol = name_or_sym.to_sym
231
- name = [@java_package, name].join('.')
232
- end
289
+ #
290
+ # @param [Symbol] symbol the class symbol
291
+ # @param [String, nil] pkg the Java class package name, or nil for the default module package
292
+ def import_domain_class(symbol, pkg=nil)
233
293
  # check if the class is already defined
234
294
  if const_defined?(symbol) then
235
295
  klass = const_get(symbol)
@@ -239,35 +299,50 @@ module CaRuby
239
299
  end
240
300
 
241
301
  # import the Java class
242
- logger.debug { "Importing #{qp} Java class #{symbol}..." }
302
+ pkg ||= @java_package
303
+ name = [pkg, symbol.to_s].join('.')
304
+ logger.debug { "Detecting whether #{symbol} is a #{pkg} Java class..." }
305
+ # Push each imported class onto a stack. When all referenced classes are imported,
306
+ # each class on the stack is post-initialized and the class structure is printed to
307
+ # the log.
308
+ @import_stack ||= []
309
+ @import_stack.push(symbol)
243
310
  begin
244
- java_import(name)
245
- rescue Exception => e
246
- raise JavaIncludeError.new("#{symbol} is not a #{qp} Java class - #{e.message}")
311
+ resource_import(name)
312
+ rescue Exception
313
+ @import_stack.pop
314
+ if symbol.to_s =~ /Annotation/ then raise end
315
+ raise JavaIncludeError.new("#{symbol} is not a #{qp} Java class - #{$!}")
247
316
  end
248
317
 
249
318
  # the imported Java class is registered as a constant in this module
250
319
  klass = const_get(symbol)
251
- # the Resource import stack
252
- @import_stack ||= []
253
- @import_stack.push klass
254
- # include the Resource mixin in the imported class
255
- inc = "include #{mixin}"
256
- klass.instance_eval(inc)
257
-
258
320
  # if we are back to top of the stack, then print the imported Resources
259
- if klass == @import_stack.first then
321
+ if symbol == @import_stack.first then
260
322
  # a referenced class could be imported on demand in the course of printing a referencing class;
261
323
  # the referenced class is then pushed onto the stack. thus, the stack can grow during the
262
324
  # course of printing, but each imported class is consumed and printed in the end.
263
325
  until @import_stack.empty? do
264
- ref = @import_stack.pop
265
- logger.debug { ref.pp_s }
326
+ # the class constant
327
+ sym = @import_stack.pop
328
+ # the imported class
329
+ kls = const_get(sym)
330
+ # invoke the call-back
331
+ imported(kls)
332
+ # print the class structure to the log
333
+ logger.info(kls.pp_s)
266
334
  end
267
335
  end
336
+
268
337
  klass
269
338
  end
270
339
 
340
+ # Call-back to perform post-import actions. This method is called after the
341
+ # given class and each of its referenced domain classes are introspected.
342
+ #
343
+ # @param [Class] the imported class
344
+ def imported(klass); end
345
+
271
346
  # The property/value matcher, e.g.:
272
347
  # host: jacardi
273
348
  # host = jacardi
@@ -287,18 +362,9 @@ module CaRuby
287
362
  if File.exists?(file) then
288
363
  file
289
364
  else
290
- logger.warn { "Default application property file not found: #{file}." }
365
+ logger.debug { "Default #{name} application property file not found: #{file}." }
291
366
  nil
292
367
  end
293
368
  end
294
-
295
- # @return [(String, Symbol)] the [package prefix, base class symbol] pair
296
- def split_class_name(class_name)
297
- # the package prefix, including the period
298
- package = Java.java_package_name(class_name)
299
- # remove the package and base class name
300
- base = package.nil? ? class_name : class_name[package.length + 1..-1]
301
- [package, base.to_sym]
302
- end
303
369
  end
304
370
  end