caruby-core 1.4.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.
Files changed (86) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +51 -0
  5. data/doc/website/css/site.css +1 -5
  6. data/doc/website/images/avatar.png +0 -0
  7. data/doc/website/images/favicon.ico +0 -0
  8. data/doc/website/images/logo.png +0 -0
  9. data/doc/website/index.html +82 -0
  10. data/doc/website/install.html +87 -0
  11. data/doc/website/quick_start.html +87 -0
  12. data/doc/website/tissue.html +85 -0
  13. data/doc/website/uom.html +10 -0
  14. data/lib/caruby.rb +3 -0
  15. data/lib/caruby/active_support/README.txt +2 -0
  16. data/lib/caruby/active_support/core_ext/string.rb +7 -0
  17. data/lib/caruby/active_support/core_ext/string/inflections.rb +167 -0
  18. data/lib/caruby/active_support/inflections.rb +55 -0
  19. data/lib/caruby/active_support/inflector.rb +398 -0
  20. data/lib/caruby/cli/application.rb +36 -0
  21. data/lib/caruby/cli/command.rb +169 -0
  22. data/lib/caruby/csv/csv_mapper.rb +157 -0
  23. data/lib/caruby/csv/csvio.rb +185 -0
  24. data/lib/caruby/database.rb +252 -0
  25. data/lib/caruby/database/fetched_matcher.rb +66 -0
  26. data/lib/caruby/database/persistable.rb +432 -0
  27. data/lib/caruby/database/persistence_service.rb +162 -0
  28. data/lib/caruby/database/reader.rb +599 -0
  29. data/lib/caruby/database/saved_merger.rb +131 -0
  30. data/lib/caruby/database/search_template_builder.rb +59 -0
  31. data/lib/caruby/database/sql_executor.rb +75 -0
  32. data/lib/caruby/database/store_template_builder.rb +200 -0
  33. data/lib/caruby/database/writer.rb +469 -0
  34. data/lib/caruby/domain/annotatable.rb +25 -0
  35. data/lib/caruby/domain/annotation.rb +23 -0
  36. data/lib/caruby/domain/attribute_metadata.rb +447 -0
  37. data/lib/caruby/domain/java_attribute_metadata.rb +160 -0
  38. data/lib/caruby/domain/merge.rb +91 -0
  39. data/lib/caruby/domain/properties.rb +95 -0
  40. data/lib/caruby/domain/reference_visitor.rb +289 -0
  41. data/lib/caruby/domain/resource_attributes.rb +528 -0
  42. data/lib/caruby/domain/resource_dependency.rb +205 -0
  43. data/lib/caruby/domain/resource_introspection.rb +159 -0
  44. data/lib/caruby/domain/resource_metadata.rb +117 -0
  45. data/lib/caruby/domain/resource_module.rb +285 -0
  46. data/lib/caruby/domain/uniquify.rb +38 -0
  47. data/lib/caruby/import/annotatable_class.rb +28 -0
  48. data/lib/caruby/import/annotation_class.rb +27 -0
  49. data/lib/caruby/import/annotation_module.rb +67 -0
  50. data/lib/caruby/import/java.rb +338 -0
  51. data/lib/caruby/migration/migratable.rb +167 -0
  52. data/lib/caruby/migration/migrator.rb +533 -0
  53. data/lib/caruby/migration/resource.rb +8 -0
  54. data/lib/caruby/migration/resource_module.rb +11 -0
  55. data/lib/caruby/migration/uniquify.rb +20 -0
  56. data/lib/caruby/resource.rb +969 -0
  57. data/lib/caruby/util/attribute_path.rb +46 -0
  58. data/lib/caruby/util/cache.rb +53 -0
  59. data/lib/caruby/util/class.rb +99 -0
  60. data/lib/caruby/util/collection.rb +1053 -0
  61. data/lib/caruby/util/controlled_value.rb +35 -0
  62. data/lib/caruby/util/coordinate.rb +75 -0
  63. data/lib/caruby/util/domain_extent.rb +49 -0
  64. data/lib/caruby/util/file_separator.rb +65 -0
  65. data/lib/caruby/util/inflector.rb +20 -0
  66. data/lib/caruby/util/log.rb +95 -0
  67. data/lib/caruby/util/math.rb +12 -0
  68. data/lib/caruby/util/merge.rb +59 -0
  69. data/lib/caruby/util/module.rb +34 -0
  70. data/lib/caruby/util/options.rb +92 -0
  71. data/lib/caruby/util/partial_order.rb +36 -0
  72. data/lib/caruby/util/person.rb +119 -0
  73. data/lib/caruby/util/pretty_print.rb +184 -0
  74. data/lib/caruby/util/properties.rb +112 -0
  75. data/lib/caruby/util/stopwatch.rb +66 -0
  76. data/lib/caruby/util/topological_sync_enumerator.rb +53 -0
  77. data/lib/caruby/util/transitive_closure.rb +45 -0
  78. data/lib/caruby/util/tree.rb +48 -0
  79. data/lib/caruby/util/trie.rb +37 -0
  80. data/lib/caruby/util/uniquifier.rb +30 -0
  81. data/lib/caruby/util/validation.rb +48 -0
  82. data/lib/caruby/util/version.rb +56 -0
  83. data/lib/caruby/util/visitor.rb +351 -0
  84. data/lib/caruby/util/weak_hash.rb +36 -0
  85. data/lib/caruby/version.rb +3 -0
  86. metadata +186 -0
@@ -0,0 +1,285 @@
1
+ require 'fileutils'
2
+ require 'caruby/util/collection'
3
+ require 'caruby/util/log'
4
+
5
+ module CaRuby
6
+ class JavaIncludeError < StandardError; end;
7
+
8
+ # The application and database connection access command line options.
9
+ ACCESS_OPTS = [
10
+ [:user, "--user USER", "the application login user"],
11
+ [:password, "--password PSWD", "the application login password"],
12
+ [:host, "--host HOST", "the application host name"],
13
+ [:database_host, "--database_host HOST", "the database host name"],
14
+ [:database_type, "--database_type TYPE", "the database type (mysql or oracle)"],
15
+ [:database_driver, "--database_driver DRIVER", "the database driver"],
16
+ [:database_port, "--database_port PORT", Integer, "the database port number"],
17
+ [:database, "--database NAME", "the database name"],
18
+ [:database_user, "--database_user USER", "the database login user"],
19
+ [:database_password, "--database_password PSWD", "the database login password"]
20
+ ]
21
+
22
+ # Importable extends a Module with Java class support.
23
+ # The calling module must implement the following instance variables:
24
+ # * +*java_package+ - the module Java package name
25
+ # * +@mixin+ - the module which scopes the domain objects
26
+ # See the ClinicalTrials module for an example.
27
+ #
28
+ # The application properties hash specifies the startup information,
29
+ # including the user, password and application Java jar library path.
30
+ # The properties are read from a property file. See CaRuby::Properties for
31
+ # more information.
32
+ #
33
+ # A Java class is imported into Ruby either by directly calling the
34
+ # extended module {#java_import} method or on demand.
35
+ # Import on demand is induced by a reference to the class, e.g.:
36
+ # module ClinicalTrials
37
+ # extend Importable
38
+ #
39
+ # def java_package
40
+ # 'org.nci.ctms'
41
+ # end
42
+ # ...
43
+ # enables references by name to a +ClinicalTrials+ Ruby class wrapper of a
44
+ # +org.nci.ctms+ Java class without an import statement, e.g.:
45
+ # ClinicalTrials::Participant.new
46
+ # without defining the +Participant+ Ruby class.
47
+ module ResourceModule
48
+ # Adds the given klass to this ResourceModule. The class is extended with ResourceMetadata methods.
49
+ def add_class(klass)
50
+ @resource_module__loaded ||= Set.new
51
+ # add superclass if necessary
52
+ unless @resource_module__loaded.include?(klass.superclass) or klass.superclass == Java::JavaLang::Object then
53
+ # the domain module includes the superclass on demand
54
+ const_get(klass.superclass.name[/\w+$/].to_sym)
55
+ end
56
+ ResourceMetadata.extend_class(klass, self)
57
+ @resource_module__loaded << klass
58
+ end
59
+
60
+ # Reinclude the mixin in all loaded classes.
61
+ def mixin_changed
62
+ @resource_module__loaded.each { |klass| klass.instance_eval { include CaRuby::Resource } }
63
+ end
64
+
65
+ def access_properties
66
+ @resource_module__props ||= load_access_properties
67
+ end
68
+
69
+ # Loads the application start-up properties in the given file path.
70
+ # The default file path is a period followed by the lower-case application name in the home directory,
71
+ # e.g. +~/.clincaltrials+.
72
+ #
73
+ # The property file format is a series of property definitions in the form _property_ +=+ _value_.
74
+ # The supported properties include the following:
75
+ # * +path+ - the application client Java jar file directories
76
+ # * +user+ - the application service login
77
+ # * +password+ - the application service password
78
+ # * +database+ - the application database name
79
+ # * +database_user+ - the application database connection userid
80
+ # * +database_password+ - the application database connection password
81
+ # * :database_host - the application database connection host (default +localhost+)
82
+ # * +database_type+ - the application database type, + mysql+ or +oracle+ (default +mysql+)
83
+ # * +database_driver+ - the application database connection driver (default is the database type default)
84
+ # * +database_port+ - the application database connection port
85
+ # # The +path+ value is one or more directories separated by a semi-colon(;) or colon (:)
86
+ # # The jar files in the +path+ directory are added to the caRuby execution Java classpath.
87
+ def load_access_properties(file=nil)
88
+ # If a file was specified, then it must exist.
89
+ if file and not File.exists?(file) then
90
+ raise ArgumentError.new("Application access properties file does not exist: #{file}")
91
+ end
92
+ # the access properties
93
+ @resource_module__props ||= {}
94
+ # If no file was specified, then try the default.
95
+ # If the default does not exist, then use the empty properties hash.
96
+ # It is not an error to omit access properties, since the application domain classes
97
+ # can still be used but not queried or saved.
98
+ file ||= default_properties_file || return
99
+
100
+ logger.info("Loading application properties from #{file}...")
101
+ File.open(file).map do |line|
102
+ # match the tolerant property definition
103
+ match = PROP_DEF_REGEX.match(line.chomp) || next
104
+ # the property [name, value] tokens
105
+ tokens = match.captures
106
+ name = tokens.first.to_sym
107
+ value = tokens.last
108
+ # capture the property
109
+ @resource_module__props[name] = value
110
+ end
111
+
112
+ # Look for environment overrides preceded by the uppercase module name, e.g. CATISSUE
113
+ # for the CaTissue module.
114
+ env_prefix = name[/\w+$/].upcase
115
+ ACCESS_OPTS.each do |spec|
116
+ # the access option symbol is the first specification item
117
+ opt = spec[0]
118
+ # the envvar, e.g. CATISSUE_USER
119
+ ev = "#{env_prefix}_#{opt.to_s.upcase}"
120
+ # the envvar value
121
+ value = ENV[ev] || next
122
+ # override the file property with the envar value
123
+ @resource_module__props[opt] = value
124
+ logger.info("Set application property #{opt} from environment variable #{ev}.")
125
+ end
126
+
127
+ # load the Java application jar path
128
+ path_ev = "#{env_prefix}_PATH"
129
+ path = ENV[path_ev] || @resource_module__props[:path]
130
+ Java.add_path(path) if path
131
+
132
+ @resource_module__props
133
+ end
134
+
135
+ # Loads the Ruby source files in the given directory.
136
+ def load_dir(dir)
137
+ # load the properties on demand
138
+ load_access_properties if @resource_module__props.nil?
139
+ # the domain class definitions
140
+ sources = Dir.glob(File.join(dir, "*.rb"))
141
+
142
+ # autoload the domain classes to ensure that definitions are picked up on demand in class hierarchy order
143
+ sym_file_hash = {}
144
+ sources.each do |file|
145
+ base_name = File.basename(file, ".rb")
146
+ sym = base_name.camelize.to_sym
147
+ sym_file_hash[sym] = file
148
+ autoload(sym, file)
149
+ end
150
+
151
+ # load the domain class definitions
152
+ sym_file_hash.each do |sym, file|
153
+ require file
154
+ end
155
+
156
+ # print the loaded classes to the log
157
+ sym_file_hash.each_key do |sym|
158
+ # it is not an error if the inferred class name is not loaded, since only the Java application classes
159
+ # are required to be the camel-case form of the file names.
160
+ klass = const_get(sym) rescue next
161
+ logger.info("#{klass.pp_s}")
162
+ end
163
+ end
164
+
165
+ # Extends the mod module with Java class support. See the class documentation for details.
166
+ def const_missing(symbol)
167
+ autoload?(symbol) ? super : import_domain_class(symbol)
168
+ end
169
+
170
+ # Imports the domain Java class with specified class name_or_sym.
171
+ # This method enables the domain class extensions for storing and retrieving domain objects.
172
+ # The class is added to this module.
173
+ #
174
+ # The optional block overrides the native Java property access wrappers.
175
+ # For example:
176
+ # ClinicalTrials.java_import('edu.wustl.catissuecore.domain.Study') do
177
+ # def study_code=(value)
178
+ # value = value.to_s if Integer === value
179
+ # setStudyCode(value)
180
+ # end
181
+ # end
182
+ # imports the ClinicalTrials Study class as ClinicalTrials::Study and overrides the
183
+ # +study_code+ setter method.
184
+ #
185
+ # Convenience aliases are added to the imported class, e.g. attribute +studyParticipantCollection+
186
+ # is aliased as +study_participants+. Specifically, each attribute reader and writer is aliased with
187
+ # the lower-case, underscore equivalent and a name ending in 'Collection' is changed to plural.
188
+ # Pluralization is smart, e.g. +studyCollection+ is aliased to +studies+ rather than +studys+.
189
+ #
190
+ # The optional aliases argument consists of additional alias => standard attribute associations.
191
+ # The optional owner_attr argument is a non-Java annotation owner attribute.
192
+ def import_domain_class(name_or_sym)
193
+ name = name_or_sym.to_s
194
+ if name.include?('.') then
195
+ symbol = name[/[A-Z]\w*$/].to_sym
196
+ else
197
+ symbol = name_or_sym.to_sym
198
+ name = [@java_package, name].join('.')
199
+ end
200
+ # check if the class is already defined
201
+ if const_defined?(symbol) then
202
+ klass = const_get(symbol)
203
+ if domain?(klass) then
204
+ logger.warn("Attempt to import #{self.qp} class twice: #{symbol}.") and return klass
205
+ end
206
+ end
207
+
208
+ # import the Java class
209
+ logger.debug { "Importing #{qp} Java class #{symbol}..." }
210
+ begin
211
+ java_import(name)
212
+ rescue Exception => e
213
+ raise JavaIncludeError.new("#{symbol} is not a #{qp} Java class - #{e.message}")
214
+ end
215
+
216
+ # the imported Java class is registered as a constant in this module
217
+ klass = const_get(symbol)
218
+ # the Resource import stack
219
+ @import_stack ||= []
220
+ @import_stack.push klass
221
+ # include the Resource mixin into the imported class
222
+ mixin = @mixin
223
+ klass.instance_eval { include mixin }
224
+ logger.info("Imported #{qp} class #{klass.qp}")
225
+ # if we are back to top of the stack, then print the imported Resources
226
+ if klass == @import_stack.first then
227
+ # a referenced class could be imported on demand in the course of printing a referencing class;
228
+ # the referenced class is then pushed onto the stack. thus, the stack can grow during the
229
+ # course of printing, but each imported class is consumed and printed in the end.
230
+ until @import_stack.empty? do
231
+ ref = @import_stack.pop
232
+ logger.debug { ref.pp_s }
233
+ end
234
+ end
235
+ klass
236
+ end
237
+
238
+ # Returns the domain class for class_name, or nil if none in this module.
239
+ def domain_type_with_name(class_name)
240
+ pkg, base = split_class_name(class_name)
241
+ return unless pkg.nil? or pkg == @java_package
242
+ begin
243
+ type = const_get(base)
244
+ rescue JavaIncludeError
245
+ # no such domain type; not an error.
246
+ # other exceptions indicate that there was a domain type but could not be loaded; these exceptions propagate up the call stack
247
+ logger.debug("#{base} is not a #{qp} Java class.")
248
+ return
249
+ end
250
+ type if type < Resource
251
+ end
252
+
253
+ private
254
+
255
+ # The property/value matcher, e.g.:
256
+ # host: jacardi
257
+ # host = jacardi
258
+ # host=jacardi
259
+ # name: J. Edgar Hoover
260
+ # but not:
261
+ # # host: jacardi
262
+ # The captures are the trimmed property and value
263
+ PROP_DEF_REGEX = /^(\w+)(?:\s*[:=]\s*)([^#]+)/
264
+
265
+ def default_properties_file
266
+ home = ENV['HOME'] || '~'
267
+ file = File.expand_path("#{home}/.#{name[/\w+$/].downcase}")
268
+ if File.exists?(file) then
269
+ file
270
+ else
271
+ logger.warn { "Default application property file not found: #{file}." }
272
+ nil
273
+ end
274
+ end
275
+
276
+ # Returns the [package prefix, base class symbol] pair.
277
+ def split_class_name(class_name)
278
+ # the package prefix, including the period
279
+ package = Java.java_package_name(class_name)
280
+ # remove the package and base class name
281
+ base = package.nil? ? class_name : class_name[package.length + 1..-1]
282
+ [package, base.to_sym]
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,38 @@
1
+ require 'singleton'
2
+ require 'caruby/util/uniquifier'
3
+ require 'caruby/util/collection'
4
+
5
+ module CaRuby
6
+ module Resource
7
+ # The Unique mix-in makes values unique within the scope of a Resource class.
8
+ module Unique
9
+ # Makes the given String value unique in the context of this object's class.
10
+ # @return nil if value is nil
11
+ # Raises TypeError if value is neither nil nor a String.
12
+ def uniquify_value(value)
13
+ unless String === value or value.nil? then
14
+ raise TypeError.new("Cannot uniquify #{qp} non-String value #{value}")
15
+ end
16
+ ResourceUniquifier.instance.uniquify(self, value)
17
+ end
18
+ end
19
+ end
20
+
21
+ # The ResourceUniquifier singleton makes Resource instance attribute values unique.
22
+ class ResourceUniquifier
23
+ include Singleton
24
+
25
+ def initialize
26
+ @cache = LazyHash.new { Hash.new }
27
+ end
28
+
29
+ # Makes the obj attribute value unique, or returns nil if value is nil.
30
+ def uniquify(obj, value)
31
+ @cache[obj.class][value] ||= value.uniquify if value
32
+ end
33
+
34
+ def clear
35
+ @cache.clear
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ require 'caruby/util/class'
2
+ require 'caruby/util/collection'
3
+ require 'caruby/import/annotation_module'
4
+
5
+ module JavaImport
6
+ # AnnotatableClass adds Annotation modules to an annotation anchor's domain module.
7
+ module AnnotatableClass
8
+
9
+ def self.extended(klass)
10
+ klass.class_eval { include CaRuby::Annotatable }
11
+ end
12
+
13
+ def annotation_modules
14
+ @annotation_modules ||= []
15
+ end
16
+
17
+ # Creates a new AnnotationModule anchored by this class for the given package and database service.
18
+ def create_annotation_module(package, service)
19
+ annotation_modules << AnnotationModule.create_annotation_module(self, package, service)
20
+ end
21
+
22
+ # Returns the class with the given unqualified name in one of this AnnotationClass's JavaImport::AnnotationModule
23
+ # modules, or nil if no such class.
24
+ def annotation_class(name)
25
+ annotation_modules.detect_value { |mod| annotation_modules.const_get(name) rescue nil }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module JavaImport
2
+ # AnnotationClass extends a domain class for an annotation class scoped by an annotation module.
3
+ # The extended class must contain the +@anchor_class+ and +@annotation_module+ instance variables.
4
+ module AnnotationClass
5
+
6
+ # Dynamically creates a new reference attribute if the given attribute symbol is the underscore
7
+ # form of this Annotation's anchor class.
8
+ #
9
+ #@param [Symbol] attribute the missing attribute
10
+ def attribute_missing(attribute)
11
+ # delegate to super to print an error if no class
12
+ super unless attribute.to_s == anchor_class.qp.underscore
13
+ # add the annotation attribute to the anchor class and the anchor attribute to this class
14
+ anchor_class.add_annotation(self)
15
+ end
16
+
17
+ # @return [Class] the annotated class
18
+ def anchor_class
19
+ @anchor_class
20
+ end
21
+
22
+ # @return [Module] the module which scopes this AnnotationClass
23
+ def annotation_module
24
+ @annotation_module
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,67 @@
1
+ require 'caruby/util/class'
2
+ require 'caruby/util/collection'
3
+ require 'caruby/import/importable'
4
+ require 'caruby/import/annotation_class'
5
+
6
+ module JavaImport
7
+ # AnnotationModule creates annotation package modules within a domain module as a context for importing annotation classes.
8
+ module AnnotationModule
9
+ include Importable
10
+
11
+ # Creates a new AnnotationModule in the given domain_module from the given annotation package and service.
12
+ def self.create_annotation_module(anchor_class, package, service)
13
+ domain_module = anchor_class.domain_module
14
+ # make parent modules for each package component
15
+ mod = package.split('.').inject(anchor_class.domain_module) do |parent, name|
16
+ mod_id = name.camelize.to_sym
17
+ if parent.const_defined?(mod_id) then
18
+ parent.const_get(mod_id)
19
+ else
20
+ parent.const_set(mod_id, Module.new)
21
+ end
22
+ end
23
+ # the terminal module is the AnnotationModule
24
+ mod.extend(self)
25
+ # initialize the java_package, service and empty access_properties
26
+ mod.module_eval do
27
+ @anchor_class = anchor_class
28
+ @java_package = package
29
+ @service = service
30
+ @resource_module__props = CaRuby::Domain::Properties.new
31
+ end
32
+ mod_name = mod.name[anchor_class.domain_module.name.length..-1]
33
+ logger.debug { "Created annotation module #{mod}." }
34
+ mod
35
+ end
36
+
37
+ def java_package
38
+ @java_package
39
+ end
40
+
41
+ def service
42
+ @service
43
+ end
44
+
45
+ def anchor_class
46
+ @anchor_class
47
+ end
48
+
49
+ def access_properties
50
+ @resource_module__props
51
+ end
52
+
53
+ # Imports the domain Java class with specified class_name by delegating to Importable and
54
+ # augmenting the importer to include CaRuby::Annotation in each imported class.
55
+ def import_domain_class(class_name)
56
+ anchor_class = @anchor_class
57
+ ann_mod = self
58
+ klass = super(class_name) do
59
+ include CaRuby::Annotation
60
+ @anchor_class = anchor_class
61
+ @annotation_module = ann_mod
62
+ yield if block_given?
63
+ end
64
+ klass.extend(AnnotationClass)
65
+ end
66
+ end
67
+ end