caruby-core 1.4.6 → 1.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt CHANGED
@@ -1,29 +1,33 @@
1
1
  === 1.4.1 / 2010-11-23
2
2
 
3
- * Initial public release
3
+ * Initial public release.
4
4
 
5
5
  === 1.4.2 / 2010-11-30
6
6
 
7
- * Minor Migrator fixes
7
+ * Minor Migrator fixes.
8
8
 
9
9
  === 1.4.3 / 2011-02-18
10
10
 
11
11
  * Refactor Persistifier
12
12
 
13
- * Add attribute filters
13
+ * Add attribute filters.
14
14
 
15
15
  === 1.4.4 / 2011-02-25
16
16
 
17
- * Support default migration option
17
+ * Support default migration option.
18
18
 
19
19
  * Merge nondomain collection value properly.
20
20
 
21
21
  === 1.4.5 / 2011-02-26
22
22
 
23
- * Fix default option
23
+ * Fix default option.
24
24
 
25
25
  === 1.4.6 / 2011-02-26
26
26
 
27
- * Upgrade to JRuby 1.5
27
+ * Upgrade to JRuby 1.5.
28
+
29
+ === 1.4.7 / 2011-03-04
30
+
31
+ * Support annotation migration.
28
32
 
29
33
 
data/README.md CHANGED
@@ -35,11 +35,6 @@ Usage
35
35
 
36
36
  See the project [Home](http://caruby.rubyforge.org) Page for usage examples.
37
37
 
38
- Changelog
39
- ---------
40
-
41
- - See +History.txt+
42
-
43
38
  Copyright
44
39
  ---------
45
40
 
@@ -4,12 +4,14 @@ require 'caruby/util/collection'
4
4
  module CaRuby
5
5
  class Database
6
6
  # Proc that matches fetched sources to targets.
7
- class FetchedMatcher < Proc
7
+ class FetchedMatcher
8
8
  # Initializes a new FetchedMatcher.
9
- def initialize
10
- super { |srcs, tgts| match_fetched(srcs, tgts) }
9
+ def match(srcs, tgts)
10
+ match_fetched(srcs, tgts)
11
11
  end
12
12
 
13
+ alias :call :match
14
+
13
15
  private
14
16
 
15
17
  # Returns a target => source match hash for the given targets and sources using
@@ -27,6 +29,7 @@ module CaRuby
27
29
  attr_md = klass.attribute_metadata(attr)
28
30
  attr_md.domain? and not attr_md.owner?
29
31
  end
32
+
30
33
  # fetch the non-owner secondary key domain attributes as necessary
31
34
  unless attrs.empty? then
32
35
  sources.each do |src|
@@ -39,6 +42,7 @@ module CaRuby
39
42
  end
40
43
  end
41
44
  end
45
+
42
46
  # match source => target based on the secondary key
43
47
  unmatched = Set === sources ? sources.dup : sources.to_set
44
48
  matches = {}
@@ -48,6 +52,7 @@ module CaRuby
48
52
  matches[src] = tgt
49
53
  unmatched.delete(src)
50
54
  end
55
+
51
56
  matches
52
57
  end
53
58
  end
@@ -93,7 +93,7 @@ module CaRuby
93
93
  return oldval if fetched.nil_or_empty?
94
94
  # merge the fetched into the attribute
95
95
  logger.debug { "Merging #{subject.qp} fetched #{attribute} value #{fetched.qp}#{' into ' + oldval.qp if oldval}..." }
96
- matches = @matcher.call(fetched.to_enum, oldval.to_enum)
96
+ matches = @matcher.match(fetched.to_enum, oldval.to_enum)
97
97
  subject.merge_attribute(attribute, fetched, matches)
98
98
  end
99
99
  end
@@ -23,8 +23,10 @@ module CaRuby
23
23
  @name = name
24
24
  ver_opt = opts[:version]
25
25
  @version = ver_opt.to_s.to_version if ver_opt
26
- @host = opts[:host] || "localhost"
26
+ @host = opts[:host] || default_host
27
+ @port = opts[:port] || 8080
27
28
  @timer = Stopwatch.new
29
+ logger.debug { "Created persistence service #{name} at #{@host}:#{@port}." }
28
30
  end
29
31
 
30
32
  ## Database access methods ##
@@ -81,7 +83,7 @@ module CaRuby
81
83
 
82
84
  # @return [ApplicationServiceProvider] the CaCORE service provider wrapped by this PersistenceService
83
85
  def app_service
84
- url = "http://#{@host}:8080/#{name}/http/remoteService"
86
+ url = "http://#{@host}:#{@port}/#{@name}/http/remoteService"
85
87
  logger.debug { "Connecting to service provider at #{url}..." }
86
88
  ApplicationServiceProvider.remote_instance(url)
87
89
  end
@@ -109,7 +111,17 @@ module CaRuby
109
111
  def dispatch
110
112
  time { yield app_service }
111
113
  end
112
-
114
+
115
+ # @return [String] the default host name
116
+ def default_host
117
+ # # TODO - extract from the service config file
118
+ # xml = JRuby.runtime.jruby_class_loader.getResourceAsStream('remoteService.xml')
119
+ # if xml then
120
+ # # parse xml file
121
+ # end
122
+ 'localhost'
123
+ end
124
+
113
125
  def query_hql(hql)
114
126
  logger.debug { "Building HQLCriteria..." }
115
127
  criteria = HQLCriteria.new(hql)
@@ -4,10 +4,6 @@ module CaRuby
4
4
  class Database
5
5
  # Proc that matches saved result sources to targets.
6
6
  class SavedMatcher < FetchedMatcher
7
- # Initializes a new SavedMatcher.
8
- def initialize
9
- super
10
- end
11
7
 
12
8
  private
13
9
 
@@ -549,6 +549,8 @@ module CaRuby
549
549
  #
550
550
  # @param (see #sync_saved)
551
551
  def sync_save_result(source, target)
552
+ # Bail if the result is the same as the source, as occurs, e.g., with caTissue annotations.
553
+ return if source == target
552
554
  logger.debug { "Synchronizing #{target} save result #{source} with the database..." }
553
555
  # If the target was created, then refetch and merge the source if necessary to reflect auto-generated
554
556
  # non-domain attribute values.
@@ -80,6 +80,7 @@ module CaRuby
80
80
  #
81
81
  # @param [String] service_name the name of the default {PersistenceService}
82
82
  # @param [{Symbol => String}] opts access options
83
+ # @option opts [String] :host application service host name
83
84
  # @option opts [String] :login application service login user
84
85
  # @option opts [String] :password application service login password
85
86
  # @example
@@ -91,9 +92,10 @@ module CaRuby
91
92
  @defaults = {}
92
93
  @user = Options.get(:user, opts)
93
94
  @password = Options.get(:password, opts)
94
- @host = Options.get(:host, opts)
95
+ host = Options.get(:host, opts)
96
+ port = Options.get(:port, opts)
95
97
  # class => service hash; default is the catissuecore app service
96
- @def_persist_svc = PersistenceService.new(service_name, :host => @host)
98
+ @def_persist_svc = PersistenceService.new(service_name, :host => host, :port => port)
97
99
  @persistence_services = [@def_persist_svc].to_set
98
100
  @cls_svc_hash = Hash.new(@def_persist_svc)
99
101
  # the create/update nested operations
@@ -73,6 +73,8 @@ module CaRuby
73
73
  def java_attributes
74
74
  @java_attrs ||= attribute_filter { |attr_md| attr_md.java_property? }
75
75
  end
76
+
77
+ alias :printable_attributes :java_attributes
76
78
 
77
79
  # @return [<Symbol>] the domain attributes
78
80
  def domain_attributes
@@ -366,19 +368,19 @@ module CaRuby
366
368
  # @yield [attr_md] the attribute selector
367
369
  # @yieldparam [AttributeMetadata] attr_md the candidate attribute
368
370
  def attribute_filter(&filter)
369
- Filter.new(@attr_md_hash, &filter)
371
+ Filter.new(@attr_md_hash, &filter)
370
372
  end
371
373
 
372
374
  # Initializes the attribute meta-data structures.
373
375
  def init_attributes
374
376
  @local_std_attr_hash = {}
375
- @alias_std_attr_map = append_parent_enum(@local_std_attr_hash) { |par| par.alias_standard_attribute_hash }
377
+ @alias_std_attr_map = append_ancestor_enum(@local_std_attr_hash) { |par| par.alias_standard_attribute_hash }
376
378
  @local_attr_md_hash = {}
377
- @attr_md_hash = append_parent_enum(@local_attr_md_hash) { |par| par.attribute_metadata_hash }
379
+ @attr_md_hash = append_ancestor_enum(@local_attr_md_hash) { |par| par.attribute_metadata_hash }
378
380
  @attributes = Enumerable::Enumerator.new(@attr_md_hash, :each_key)
379
381
  @local_mndty_attrs = Set.new
380
382
  @local_defaults = {}
381
- @defaults = append_parent_enum(@local_defaults) { |par| par.defaults }
383
+ @defaults = append_ancestor_enum(@local_defaults) { |par| par.defaults }
382
384
  end
383
385
 
384
386
  # Creates the given aliases to attributes.
@@ -472,9 +474,13 @@ module CaRuby
472
474
  @local_std_attr_hash[aliaz.to_sym] = std_attr
473
475
  end
474
476
 
475
- # Returns a new Enumerable which appends the evaluation of the given block in the parent
476
- # metadata context. The default enum is the evaluation of the given block on this Metadata.
477
- def append_parent_enum(enum)
477
+ # Appends to the given enumerable the result of evaluating the block given to this method
478
+ # on the superclass, if the superclass is a {Resource}.
479
+ #
480
+ # @param [Enumerable] enum the base collection
481
+ # @return [Enumerable] the {Enumerable#union} of the base collection with the superclass
482
+ # collection, if applicable
483
+ def append_ancestor_enum(enum)
478
484
  superclass < Resource ? enum.union(yield(superclass)) : enum
479
485
  end
480
486
 
@@ -506,7 +512,7 @@ module CaRuby
506
512
  # remove autogenerated or optional attributes
507
513
  mandatory.delete_if { |attr| attribute_metadata(attr).autogenerated? or attribute_metadata(attr).optional? }
508
514
  @local_mndty_attrs.merge!(mandatory)
509
- append_parent_enum(@local_mndty_attrs) { |par| par.mandatory_attributes }
515
+ append_ancestor_enum(@local_mndty_attrs) { |par| par.mandatory_attributes }
510
516
  end
511
517
 
512
518
  # Raises a NameError. Domain classes can override this method to dynamically create a new reference attribute.
@@ -10,6 +10,7 @@ module CaRuby
10
10
  [:user, "--user USER", "the application login user"],
11
11
  [:password, "--password PSWD", "the application login password"],
12
12
  [:host, "--host HOST", "the application host name"],
13
+ [:port, "--port PORT", "the application port number"],
13
14
  [:database_host, "--database_host HOST", "the database host name"],
14
15
  [:database_type, "--database_type TYPE", "the database type (mysql or oracle)"],
15
16
  [:database_driver, "--database_driver DRIVER", "the database driver"],
@@ -27,7 +28,7 @@ module CaRuby
27
28
  #
28
29
  # The application properties hash specifies the startup information,
29
30
  # including the user, password and application Java jar library path.
30
- # The properties are read from a property file. See CaRuby::Properties for
31
+ # The properties are read from a property file. See {Properties} for
31
32
  # more information.
32
33
  #
33
34
  # A Java class is imported into Ruby either by directly calling the
@@ -45,7 +46,9 @@ module CaRuby
45
46
  # ClinicalTrials::Participant.new
46
47
  # without defining the +Participant+ Ruby class.
47
48
  module ResourceModule
48
- # Adds the given klass to this ResourceModule. The class is extended with ResourceMetadata methods.
49
+ # Adds the given class to this ResourceModule. The class is extended with ResourceMetadata methods.
50
+ #
51
+ # @param [Class] the {Resource} class to add
49
52
  def add_class(klass)
50
53
  @rsc_classes ||= Set.new
51
54
  # add superclass if necessary
@@ -65,7 +68,7 @@ module CaRuby
65
68
  # @return [{Symbol => Object}] the caBIG application access properties
66
69
  # @see #load_access_properties
67
70
  def access_properties
68
- @resource_module__props ||= load_access_properties
71
+ @rsc_props ||= load_access_properties
69
72
  end
70
73
 
71
74
  # Loads the application start-up properties in the given file path.
@@ -97,7 +100,7 @@ module CaRuby
97
100
  raise ArgumentError.new("Application access properties file does not exist: #{file}")
98
101
  end
99
102
  # the access properties
100
- @resource_module__props ||= {}
103
+ props ||= {}
101
104
  # If no file was specified, then try the default.
102
105
  # If the default does not exist, then use the empty properties hash.
103
106
  # It is not an error to omit access properties, since the application domain classes
@@ -113,9 +116,8 @@ module CaRuby
113
116
  name = tokens.first.to_sym
114
117
  value = tokens.last
115
118
  # capture the property
116
- @resource_module__props[name] = value
119
+ props[name] = value
117
120
  end
118
-
119
121
  # Look for environment overrides preceded by the uppercase module name, e.g. CATISSUE
120
122
  # for the CaTissue module.
121
123
  env_prefix = name[/\w+$/].upcase
@@ -127,22 +129,22 @@ module CaRuby
127
129
  # the envvar value
128
130
  value = ENV[ev] || next
129
131
  # override the file property with the envar value
130
- @resource_module__props[opt] = value
132
+ props[opt] = value
131
133
  logger.info("Set application property #{opt} from environment variable #{ev}.")
132
134
  end
133
135
 
134
136
  # load the Java application jar path
135
137
  path_ev = "#{env_prefix}_PATH"
136
- path = ENV[path_ev] || @resource_module__props[:path]
138
+ path = ENV[path_ev] || props[:path]
137
139
  Java.add_path(path) if path
138
140
 
139
- @resource_module__props
141
+ props
140
142
  end
141
143
 
142
144
  # Loads the Ruby source files in the given directory.
143
145
  def load_dir(dir)
144
146
  # load the properties on demand
145
- load_access_properties if @resource_module__props.nil?
147
+ load_access_properties if @rsc_props.nil?
146
148
  # the domain class definitions
147
149
  sources = Dir.glob(File.join(dir, "*.rb"))
148
150
 
@@ -156,7 +158,7 @@ module CaRuby
156
158
  end
157
159
 
158
160
  # load the domain class definitions
159
- sym_file_hash.each do |sym, file|
161
+ sym_file_hash.to_a.each do |sym, file|
160
162
  require file
161
163
  end
162
164
 
@@ -168,6 +170,11 @@ module CaRuby
168
170
  logger.info("#{klass.pp_s}")
169
171
  end
170
172
  end
173
+
174
+ def java_import(klass)
175
+ # JRuby 1.4.x does not support a class argument
176
+ Class === klass ? super(klass.java_class.name) : super
177
+ end
171
178
 
172
179
  # Extends the mod module with Java class support. See the class documentation for details.
173
180
  #
@@ -199,7 +206,7 @@ module CaRuby
199
206
  #
200
207
  # The optional block overrides the native Java property access wrappers.
201
208
  # For example:
202
- # ClinicalTrials.java_import('edu.wustl.catissuecore.domain.Study') do
209
+ # ClinicalTrials.java_import Java::edu.wustl.catissuecore.domain.Study do
203
210
  # def study_code=(value)
204
211
  # value = value.to_s if Integer === value
205
212
  # setStudyCode(value)
@@ -271,6 +278,9 @@ module CaRuby
271
278
  # The captures are the trimmed property and value
272
279
  PROP_DEF_REGEX = /^(\w+)(?:\s*[:=]\s*)([^#]+)/
273
280
 
281
+ # @return [String] the default application properties file, given by +~/.+_name_,
282
+ # where _name_ is the underscore unqualified module name, e.g. +~/.catissue+
283
+ # for module +CaTissue+
274
284
  def default_properties_file
275
285
  home = ENV['HOME'] || '~'
276
286
  file = File.expand_path("#{home}/.#{name[/\w+$/].downcase}")
@@ -15,22 +15,36 @@ module Java
15
15
  # Adds the directories in the given path and all Java jar files contained in the directories
16
16
  # to the execution classpath.
17
17
  #
18
- # @param path the colon or semi-colon separated directories
18
+ # @param [String] path the colon or semi-colon separated directories
19
19
  def self.add_path(path)
20
20
  # the path directories
21
21
  dirs = path.split(/[:;]/).map { |dir| File.expand_path(dir) }
22
22
  # Add all jars found anywhere within the directories to the the classpath.
23
23
  add_jars(*dirs)
24
24
  # Add the directories to the the classpath.
25
- dirs.each { |dir| $CLASSPATH << dir }
25
+ dirs.each { |dir| add_to_classpath(dir) }
26
26
  end
27
27
 
28
28
  # Adds the jars in the directories to the execution class path.
29
29
  #
30
- # @param directories the directories containing jars to add
30
+ # @param [<String>] directories the directories containing jars to add
31
31
  def self.add_jars(*directories)
32
32
  directories.each do |dir|
33
- Dir[File.join(dir , "**", "*.jar")].each { |jar| $CLASSPATH << jar }
33
+ Dir[File.join(dir , "**", "*.jar")].each { |jar| add_to_classpath(jar) }
34
+ end
35
+ end
36
+
37
+ # Adds the given jar file or directory to the classpath.
38
+ #
39
+ # @param [String] file the jar file or directory to add
40
+ def self.add_to_classpath(file)
41
+ if file =~ /.jar$/ then
42
+ # require is preferred to classpath append for a jar file
43
+ require file
44
+ else
45
+ # A directory must end in a slash since JRuby uses an URLClassLoader.
46
+ if File.directory?(file) and not file =~ /\/$/ then file = file + '/' end
47
+ $CLASSPATH << file
34
48
  end
35
49
  end
36
50
 
@@ -118,55 +118,49 @@ module CaRuby
118
118
  # @param [{Symbol => String}] mth_hash a hash that associates this domain object's
119
119
  # attributes to migration method names
120
120
  def migrate_references(row, migrated, mth_hash=nil)
121
- self.class.saved_independent_attributes.each do |attr|
122
- ref = migratable__reference_value(attr, migrated)
123
- migratable__set_reference(attr, ref, row, mth_hash) if ref
124
- end
125
- self.class.unidirectional_dependent_attributes.each do |attr|
126
- ref = migratable__reference_value(attr, migrated)
127
- migratable__set_reference(attr, ref, row, mth_hash) if ref
128
- end
121
+ migratable__set_references(self.class.saved_independent_attributes, row, migrated, mth_hash)
122
+ migratable__set_references(self.class.unidirectional_dependent_attributes, row, migrated, mth_hash)
129
123
  end
130
124
 
131
125
  private
132
126
 
133
- # @param [Symbol] attribute the reference attribute to get
127
+ # @param [AttributeMetadata::Filter] the attributes to set
128
+ # @param row (see #migrate_references)
129
+ # @param migrated (see #migrate_references)
130
+ # @param mth_hash (see #migrate_references)
131
+ def migratable__set_references(attr_filter, row, migrated, mth_hash=nil)
132
+ attr_filter.each_pair do |attr, attr_md|
133
+ # the target value
134
+ ref = migratable__target_value(attr_md, row, migrated, mth_hash) || next
135
+ if attr_md.collection? then
136
+ # the current value
137
+ value = send(attr_md.reader) || next
138
+ value << ref
139
+ logger.debug { "Added migrated #{ref.qp} to #{qp} #{attribute}." }
140
+ else
141
+ set_attribute(attr, ref)
142
+ logger.debug { "Set #{qp} #{attr} to migrated #{ref.qp}." }
143
+ end
144
+ end
145
+ end
146
+
147
+ # @param [AttributeMetadata] attr_md the reference attribute
148
+ # @param row (see #migrate_references)
134
149
  # @param migrated (see #migrate_references)
135
- # @return [Resource, nil] the migrated value to which the attribute will be set
136
- def migratable__reference_value(attribute, migrated)
137
- # skip non-nil attributes
138
- return if send(attribute)
139
- # the attribute metadata, used for type information
140
- attr_md = self.class.attribute_metadata(attribute)
141
- # skip collection attributes
142
- return if attr_md.collection?
150
+ # @param mth_hash (see #migrate_references)
151
+ # @return [Resource, nil] the migrated instance of the given class, or nil if there is not
152
+ # exactly one such instance
153
+ def migratable__target_value(attr_md, row, migrated, mth_hash=nil)
143
154
  # the migrated references which are instances of the attribute type
144
155
  refs = migrated.select { |other| other != self and attr_md.type === other }
145
156
  # skip ambiguous references
146
157
  return unless refs.size == 1
147
158
  # the single reference
148
159
  ref = refs.first
149
- end
150
-
151
- # Sets the given migrated domain object attribute to the given reference.
152
- #
153
- # If the attribute is associated to a method in mth_hash, then that method is called on
154
- # the migrated instance and input row. The attribute is set to the method return value.
155
- # mth_hash includes an entry for each +migrate_+_attribute_ method defined by this
156
- # Resource's class.
157
- #
158
- # @param [Symbol] (see #migratable__reference_value)
159
- # @param [Resource] ref the migrated reference
160
- # @param row (see #migrate_references)
161
- # @param mth_hash (see #migrate_references)
162
- def migratable__set_reference(attribute, ref, row, mth_hash=nil)
163
- # the shim method, if any
164
- mth = mth_hash[attribute] if mth_hash
160
+ # the shim method, if any
161
+ mth = mth_hash[attr_md.to_sym] if mth_hash
165
162
  # if there is a shim method, then call it
166
- ref = send(mth, ref, row) if mth and respond_to?(mth)
167
- return if ref.nil?
168
- logger.debug { "Setting #{qp} #{attribute} to migrated #{ref.qp}..." }
169
- set_attribute(attribute, ref)
163
+ mth && respond_to?(mth) ? send(mth, ref, row) : ref
170
164
  end
171
165
  end
172
166
  end
@@ -99,6 +99,7 @@ module CaRuby
99
99
  end
100
100
 
101
101
  def parse_options(opts)
102
+ logger.debug { "Migrator options: #{opts.qp}" }
102
103
  @fld_map_file = opts[:mapping]
103
104
  raise MigrationError.new("Migrator missing required field mapping file parameter") if @fld_map_file.nil?
104
105
  @def_file = opts[:defaults]
@@ -145,6 +146,7 @@ module CaRuby
145
146
  end
146
147
  logger.info { "Migration paths:\n#{print_hash.pp_s}" }
147
148
  logger.info { "Migration creatable classes: #{@creatable_classes.qp}." }
149
+ unless @def_hash.empty? then logger.info { "Migration defaults: #{@def_hash.qp}." } end
148
150
 
149
151
  # add shim modifiers
150
152
  load_shims(@shims)
@@ -540,7 +542,7 @@ module CaRuby
540
542
  # collect the class => path => value entries
541
543
  map = LazyHash.new { Hash.new }
542
544
  config.each do |path_s, value|
543
- next if value.blank?
545
+ next if value.nil_or_empty?
544
546
  klass, path = create_attribute_path(path_s)
545
547
  map[klass][path] = value
546
548
  end
@@ -555,7 +557,7 @@ module CaRuby
555
557
  names = path_s.split('.')
556
558
  # if the path starts with a capitalized class name, then resolve the class.
557
559
  # otherwise, the target class is the start of the path.
558
- klass = names.first =~ /^[A-Z]/ ? @target_class.domain_module.const_get(names.shift) : @target_class
560
+ klass = names.first =~ /^[A-Z]/ ? class_for_name(names.shift) : @target_class
559
561
  # there must be at least one attribute
560
562
  if names.empty? then
561
563
  raise MigrationError.new("Attribute entry in migration configuration is not in <class>.<attribute> format: #{value}")
@@ -574,6 +576,13 @@ module CaRuby
574
576
  # important, since the class must be instantiated.
575
577
  [klass, path]
576
578
  end
579
+
580
+ def class_for_name(name)
581
+ # navigate through the scope to the final class
582
+ name.split('::').inject(@target_class.domain_module) do |scope, cnm|
583
+ scope.const_get(cnm)
584
+ end
585
+ end
577
586
 
578
587
  # @return a new class => [paths] hash from the migration fields configuration map
579
588
  def create_class_paths_hash(fld_map, def_map)
@@ -33,6 +33,8 @@ module CaRuby
33
33
  "#{self.class.qp}@#{proxy_object_id}"
34
34
  end
35
35
 
36
+ alias :qp :print_class_and_id
37
+
36
38
  # Sets the default attribute values for this domain object and its dependents. If this Resource
37
39
  # does not have an identifier, then missing attributes are set to the values defined by
38
40
  # {ResourceAttributes#add_attribute_defaults}.
@@ -54,25 +56,22 @@ module CaRuby
54
56
  end
55
57
 
56
58
  # Validates this domain object and its #{ResourceAttributes.unproxied_cascaded_attributes}
57
- # for completeness prior to a database create or update operation.
58
- # The object is valid if it contains a non-nil value for each mandatory property.
59
- # Objects which have already been validated are skipped.
59
+ # for completeness prior to a database create operation.
60
+ # An object without an identifer is valid if it contains a non-nil value for each mandatory property.
61
+ # Objects which have an identifier or have already been validated are skipped.
62
+ #
63
+ # Subclasses should not override this method, but override the private {#local_validate} instead.
60
64
  #
61
65
  # @return [Resource] this domain object
62
- # @raise [ValidationError] if a mandatory attribute value is missing
66
+ # @raise (see #local_validate)
63
67
  def validate
64
- unless @validated then
65
- logger.debug { "Validating #{qp} required attributes #{self.mandatory_attributes.to_a.to_series}..." }
66
- invalid = missing_mandatory_attributes
67
- unless invalid.empty? then
68
- logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
69
- raise ValidationError.new("Required attribute value missing for #{self}: #{invalid.join(', ')}")
70
- end
68
+ if identifier.nil? and not @validated then
69
+ validate_local
70
+ @validated = true
71
71
  end
72
72
  self.class.unproxied_cascaded_attributes.each do |attr|
73
73
  send(attr).enumerate { |dep| dep.validate }
74
74
  end
75
- @validated = true
76
75
  self
77
76
  end
78
77
 
@@ -550,6 +549,20 @@ module CaRuby
550
549
  merge_attributes(self.class.defaults)
551
550
  end
552
551
 
552
+ # Validates that this domain contains a non-nil value for each mandatory property.
553
+ #
554
+ # Subclasses can override this method for additional validation, but should call super first.
555
+ #
556
+ # @raise [ValidationError] if a mandatory attribute value is missing
557
+ def validate_local
558
+ logger.debug { "Validating #{qp} required attributes #{self.mandatory_attributes.to_a.to_series}..." }
559
+ invalid = missing_mandatory_attributes
560
+ unless invalid.empty? then
561
+ logger.error("Validation of #{qp} unsuccessful - missing #{invalid.join(', ')}:\n#{dump}")
562
+ raise ValidationError.new("Required attribute value missing for #{self}: #{invalid.join(', ')}")
563
+ end
564
+ end
565
+
553
566
  # Enumerates the dependents for setting defaults. Subclasses can override if the
554
567
  # dependents must be visited in a certain order.
555
568
  alias :each_defaults_dependent :each_dependent
@@ -574,7 +587,7 @@ module CaRuby
574
587
  def initialize(base, visited=Set.new, &selector)
575
588
  @base = base
576
589
  @visited = visited << base
577
- @selector = selector || Proc.new { |ref| ref.class.java_attributes }
590
+ @selector = selector || Proc.new { |ref| ref.class.printable_attributes }
578
591
  end
579
592
 
580
593
  def pretty_print(q)
@@ -92,6 +92,13 @@ module Enumerable
92
92
  detect { true }
93
93
  end
94
94
 
95
+ # Returns the last enumerated item in this Enumerable, or nil if this Enumerable is empty.
96
+ #
97
+ # This method is functionally equivalent to +to_a.last+ but is more concise and efficient.
98
+ def last
99
+ detect { true }
100
+ end
101
+
95
102
  # Returns the count of items enumerated in this Enumerable.
96
103
  #
97
104
  # This method is functionally equivalent to +to_a.size+ but is more concise and efficient
@@ -1,4 +1,5 @@
1
- require 'generator'
1
+ # JRuby alert - SyncEnumerator moved from generator to REXML in JRuby 1.5
2
+ require 'rexml/document'
2
3
 
3
4
  # A Coordinate is a convenience Array wrapper class with aliased #x, #y and {#z} dimensions.
4
5
  class Coordinate < Array
@@ -58,7 +59,7 @@ class Coordinate < Array
58
59
  return true if equal?(other)
59
60
  raise TypeError.new("Can't compare #{self} with #{other} since it is not a Coordinate") unless Coordinate === other
60
61
  raise ArgumentError.new("Can't compare #{self} with #{other} since it has a different dimension count") unless size == other.size
61
- SyncEnumerator.new(self.reverse, other.reverse).each_with_index do |pair, index|
62
+ REXML::SyncEnumerator.new(self.reverse, other.reverse).each_with_index do |pair, index|
62
63
  dim = pair.first
63
64
  odim = pair.last
64
65
  raise ArgumentError.new("Can't compare #{self} with missing dimension #{index} to #{other}") unless dim
@@ -0,0 +1,28 @@
1
+ class String
2
+ # @return [Integer] the integer equivalent of this roman numeral
3
+ # @raise ArgumentError if this String is not a roman numeral in the range I-X
4
+ def to_arabic
5
+ case self
6
+ when /^(I{0,3})$/ then $1.size
7
+ when /^(I{0,3})(V|X)$/ then ROMAN_UNITS[$2] - $1.size
8
+ when /^(V)(I{0,3})$/ then ROMAN_UNITS[$1] + $2.size
9
+ else raise ArgumentError.new("#{self} is not a roman numeral in the range I-X")
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ ROMAN_UNITS = {'I' => 1, 'V' => 5, 'X' => 10}
16
+ end
17
+
18
+ class Integer
19
+ # @return [String] the roman numeral equivalent of this integer
20
+ def to_roman
21
+ if self < 1 or self > 10 then raise ArgumentError.new("#{self} cannot be converted to a roman numeral in the range I-X")
22
+ elsif self < 4 then 'I' * self
23
+ elsif self < 6 then ('I' * (5 - self)) + 'V'
24
+ elsif self < 9 then 'V' + ('I' * (self - 5))
25
+ else ('I' * (10 - self)) + 'X'
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module CaRuby
2
- VERSION = "1.4.6"
2
+ VERSION = "1.4.7"
3
3
  end
@@ -0,0 +1,18 @@
1
+ $:.unshift 'lib'
2
+
3
+ require "test/unit"
4
+ require 'caruby/util/roman'
5
+
6
+ class RomanTest < Test::Unit::TestCase
7
+ def test_to_arabic
8
+ ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ].each_with_index do |s, n|
9
+ assert_equal(n + 1, s.to_arabic, "Conversion of #{s} incorrect")
10
+ end
11
+ end
12
+
13
+ def test_to_roman
14
+ ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ].each_with_index do |s, n|
15
+ assert_equal(s, (n + 1).to_roman, "Conversion of #{n + 1} incorrect")
16
+ end
17
+ end
18
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: caruby-core
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.4.6
5
+ version: 1.4.7
6
6
  platform: ruby
7
7
  authors:
8
8
  - OHSU
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-02-26 00:00:00 -08:00
13
+ date: 2011-03-04 00:00:00 -08:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -25,7 +25,7 @@ dependencies:
25
25
  type: :runtime
26
26
  version_requirements: *id001
27
27
  - !ruby/object:Gem::Dependency
28
- name: fastercsv
28
+ name: dbd-jdbc
29
29
  prerelease: false
30
30
  requirement: &id002 !ruby/object:Gem::Requirement
31
31
  none: false
@@ -36,7 +36,7 @@ dependencies:
36
36
  type: :runtime
37
37
  version_requirements: *id002
38
38
  - !ruby/object:Gem::Dependency
39
- name: uom
39
+ name: fastercsv
40
40
  prerelease: false
41
41
  requirement: &id003 !ruby/object:Gem::Requirement
42
42
  none: false
@@ -46,6 +46,17 @@ dependencies:
46
46
  version: "0"
47
47
  type: :runtime
48
48
  version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: uom
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ type: :runtime
59
+ version_requirements: *id004
49
60
  description: " caRuby is a JRuby facade for interaction with caBIG applications.\n"
50
61
  email: caruby.org@gmail.com
51
62
  executables: []
@@ -117,6 +128,7 @@ files:
117
128
  - lib/caruby/util/person.rb
118
129
  - lib/caruby/util/pretty_print.rb
119
130
  - lib/caruby/util/properties.rb
131
+ - lib/caruby/util/roman.rb
120
132
  - lib/caruby/util/stopwatch.rb
121
133
  - lib/caruby/util/topological_sync_enumerator.rb
122
134
  - lib/caruby/util/transitive_closure.rb
@@ -151,6 +163,7 @@ files:
151
163
  - test/lib/caruby/util/person_test.rb
152
164
  - test/lib/caruby/util/pretty_print_test.rb
153
165
  - test/lib/caruby/util/properties_test.rb
166
+ - test/lib/caruby/util/roman_test.rb
154
167
  - test/lib/caruby/util/stopwatch_test.rb
155
168
  - test/lib/caruby/util/topological_sync_enumerator_test.rb
156
169
  - test/lib/caruby/util/transitive_closure_test.rb
@@ -188,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
201
  requirements: []
189
202
 
190
203
  rubyforge_project: caruby
191
- rubygems_version: 1.5.2
204
+ rubygems_version: 1.5.3
192
205
  signing_key:
193
206
  specification_version: 3
194
207
  summary: Ruby facade for caBIG applications.