caruby-core 1.4.7 → 1.4.9

Sign up to get free protection for your applications and to get access to all the features.
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