caruby-core 1.5.3 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.md CHANGED
@@ -1,6 +1,10 @@
1
1
  This history lists major release themes. See the GitHub Commits (https://github.com/caruby/core)
2
2
  for change details.
3
3
 
4
+ 1.5.4 / 2011-07-19
5
+ ------------------
6
+ * Add migration value filter option.
7
+
4
8
  1.5.3 / 2011-07-08
5
9
  ------------------
6
10
  * Clean up documentation.
@@ -32,11 +32,12 @@ module CaRuby
32
32
  #
33
33
  # Built-in options include the following:
34
34
  # * --help : print the help message and exit
35
- # * --version : print the version and exit
35
+ # * --verbose : print additional information to the console
36
36
  # * --log FILE : log file
37
37
  # * --debug : print debug messages to the log
38
38
  # * --file FILE: configuration file containing other options
39
39
  # * --quiet: suppress printing messages to stdout
40
+ #
40
41
  # This class processes these built-in options, with the exception of +--version+,
41
42
  # which is a subclass responsibility. Subclasses are responsible for
42
43
  # processing any remaining options.
@@ -321,15 +321,14 @@ module CaRuby
321
321
  return
322
322
  end
323
323
  @transients << obj
324
-
325
324
  logger.debug { "Fetching #{obj.qp} from the database..." }
326
325
  fetched = fetch_object(obj) || return
327
- # fetch_object can return obj; if so, then done
326
+ # fetch_object can return obj; if so, then done, otherwise, merge fetched.
328
327
  return obj if obj.equal?(fetched)
329
328
 
330
329
  logger.debug { "Fetch #{obj.qp} matched database object #{fetched}." }
331
330
  @transients.delete(obj)
332
- # recursively copy the nondomain attributes, esp. the identifer, of the fetched domain object references
331
+ # recursively copy the nondomain attributes, esp. the identifer, of the fetched domain object references
333
332
  merge_fetched(fetched, obj)
334
333
  # Inject the lazy loader for loadable domain reference attributes.
335
334
  persistify(obj, fetched)
@@ -24,14 +24,14 @@ module CaRuby
24
24
  logger.debug { "Building search template for #{obj.qp}..." }
25
25
  hash ||= obj.value_hash(obj.class.searchable_attributes)
26
26
  # the searchable attribute => value hash
27
- ref_hash, nonref_hash = hash.hash_partition { |attr, value| Resource === value }
27
+ rh, nrh = hash.split { |attr, value| Resource === value }
28
28
  # make the search template from the non-reference attributes
29
- tmpl = obj.class.new.merge_attributes(nonref_hash)
29
+ tmpl = obj.class.new.merge_attributes(nrh)
30
30
  # get references for the search template
31
- unless ref_hash.empty? then
32
- logger.debug { "Collecting search reference parameters for #{obj.qp} from attributes #{ref_hash.keys.to_series}..." }
31
+ unless rh.empty? then
32
+ logger.debug { "Collecting search reference parameters for #{obj.qp} from attributes #{rh.keys.to_series}..." }
33
33
  end
34
- ref_hash.each { |attr, ref| add_search_template_reference(tmpl, ref, attr) }
34
+ rh.each { |attr, ref| add_search_template_reference(tmpl, ref, attr) }
35
35
  tmpl
36
36
  end
37
37
 
@@ -34,6 +34,9 @@ module CaRuby
34
34
  # @option opts [String] :database_driver_class the optional DBI connect driver class name
35
35
  # @raise [CaRuby::ConfigurationError] if an option is invalid
36
36
  def initialize(opts)
37
+ if opts.empty? then
38
+ raise CaRuby::ConfigurationError.new("The caRuby database connection properties were not found.")
39
+ end
37
40
  app_host = Options.get(:host, opts, 'localhost')
38
41
  db_host = Options.get(:database_host, opts, app_host)
39
42
  db_type = Options.get(:database_type, opts, 'mysql')
@@ -52,9 +55,10 @@ module CaRuby
52
55
  :database_type => db_type,
53
56
  :database_port => db_port,
54
57
  :database_driver => db_driver,
55
- :database_driver_class => @driver_class
56
- }
57
- logger.debug { "Database connection options (excluding password): #{eff_opts.qp}" }
58
+ :database_driver_class => @driver_class,
59
+ :address => @address
60
+ }
61
+ logger.debug { "Database connection parameters (excluding password): #{eff_opts.qp}" }
58
62
  end
59
63
 
60
64
  # Connects to the database, yields the DBI handle to the given block and disconnects.
@@ -139,6 +139,7 @@ module CaRuby
139
139
  def ensure_exists(obj)
140
140
  raise ArgumentError.new("Database ensure_exists is missing a domain object argument") if obj.nil_or_empty?
141
141
  obj.enumerate { |ref| find(ref, :create) unless ref.identifier }
142
+
142
143
  end
143
144
 
144
145
  private
@@ -286,7 +287,6 @@ module CaRuby
286
287
  logger.debug { "Ensuring that created #{obj.qp} references exist: #{refs.qp}..." } unless refs.empty?
287
288
  refs.each { |ref| ensure_exists(ref) }
288
289
  end
289
-
290
290
  obj
291
291
  end
292
292
 
@@ -350,7 +350,7 @@ module CaRuby
350
350
  # * the attribute references a {#pending_create?} save context.
351
351
  #
352
352
  # @param obj (see #create)
353
- # @param [Attribute] attr_md candidate attribute metadata
353
+ # @param [Attribute] attr_md the candidate attribute metadata
354
354
  # @return [Boolean] whether the attribute should not be included in the create template
355
355
  def exclude_pending_create_attribute?(obj, attr_md)
356
356
  attr_md.independent? and
@@ -413,7 +413,7 @@ module CaRuby
413
413
  # update a cascaded dependent by updating the owner
414
414
  owner = cascaded_owner(obj)
415
415
  if owner then return update_cascaded_dependent(owner, obj) end
416
-
416
+
417
417
  # Not cascaded dependent; update using a template,
418
418
  tmpl = build_update_template(obj)
419
419
  # call the caCORE service with an obj update template
@@ -559,10 +559,9 @@ module CaRuby
559
559
  logger.debug { "Updating saved #{target} to store unsaved attributes..." }
560
560
  # call update_object(saved) rather than update(saved) to bypass the redundant update check
561
561
  perform(:update, target) { update_object(target) }
562
- else
563
- # recursively save the dependents
564
- save_changed_dependents(target)
565
562
  end
563
+ # recursively save the dependents
564
+ save_changed_dependents(target)
566
565
  end
567
566
 
568
567
  # Synchronizes the given saved target result source with the database content.
@@ -685,9 +684,9 @@ module CaRuby
685
684
  # @param [Resource] obj the owner domain object
686
685
  def save_changed_dependents(obj)
687
686
  obj.class.dependent_attributes.each do |attr|
688
- deps = obj.send(attr).to_enum
689
- logger.debug { "Saving the #{obj} #{attr} dependents #{deps.qp(:single_line)} which have changed..." } unless deps.empty?
690
- deps.each { |dep| save_dependent_if_changed(obj, attr, dep) }
687
+ deps = obj.send(attr)
688
+ logger.debug { "Saving the #{obj} #{attr} dependents #{deps.to_enum.qp(:single_line)} which have changed..." } unless deps.nil_or_empty?
689
+ deps.enumerate { |dep| save_dependent_if_changed(obj, attr, dep) }
691
690
  end
692
691
  end
693
692
 
@@ -127,7 +127,7 @@ module CaRuby
127
127
  # The inverse relation is symmetric, i.e. the inverse of the referenced Attribute
128
128
  # is set to this Attribute's subject attribute.
129
129
  #
130
- # @param attribute the inverse attribute
130
+ # @param [Symbol, nil] attribute the inverse attribute
131
131
  def inverse=(attribute)
132
132
  return if inverse == attribute
133
133
  # if no attribute, then the clear the existing inverse, if any
@@ -522,9 +522,12 @@ module CaRuby
522
522
  def clear_inverse
523
523
  return unless @inv_md
524
524
  logger.debug { "Clearing #{@declarer.qp}.#{self} inverse #{type.qp}.#{inverse}..." }
525
- inv_inv_md = @inv_md.inverse_metadata
525
+ # Capture the inverse before unsetting it.
526
+ inv_md = @inv_md
527
+ # Unset the inverse.
526
528
  @inv_md = nil
527
- if inv_inv_md then inv_inv_md.inverse = nil end
529
+ # Clear the inverse of the inverse.
530
+ inv_md.inverse = nil
528
531
  logger.debug { "Cleared #{@declarer.qp}.#{self} inverse." }
529
532
  end
530
533
 
@@ -530,8 +530,13 @@ module CaRuby
530
530
  def remove_attribute(attribute)
531
531
  std_attr = standard_attribute(attribute)
532
532
  # if the attribute is local, then delete it, otherwise filter out the superclass attribute
533
- if @local_attr_md_hash.delete(std_attr) then
533
+ attr_md = @local_attr_md_hash.delete(std_attr)
534
+ if attr_md then
535
+ # clear the inverse, if any
536
+ attr_md.inverse = nil
537
+ # remove from the mandatory attributes, if necessary
534
538
  @local_mndty_attrs.delete(std_attr)
539
+ # remove from the attribute => metadata hash
535
540
  @local_std_attr_hash.delete_if { |aliaz, attr| attr == std_attr }
536
541
  else
537
542
  # Filter the superclass hashes.
@@ -39,8 +39,8 @@ module CaRuby
39
39
  logger.debug { "Detecting whether #{symbol} is a #{@pkg} Java class..." }
40
40
  # Append the symbol to the package to make the Java class name.
41
41
  begin
42
- klass = eval "Java::#{@pkg}.#{symbol}"
43
- resource_import klass
42
+ klass = java_import "#{@pkg}.#{symbol}"
43
+ resource_import(klass)
44
44
  rescue NameError
45
45
  logger.debug { "#{symbol} is not recognized as a #{@pkg} Java class - #{$!}\n#{caller.qp}." }
46
46
  super
@@ -53,7 +53,7 @@ module CaRuby
53
53
  # The Java class is assumed to be defined in this module's package.
54
54
  # This module's mixin is added to the class.
55
55
  #
56
- # @param [String] class_or_name the source directory
56
+ # @param [Class] klass the source directory
57
57
  # @raise [NameError] if the symbol does not correspond to a Java class
58
58
  # in this module's package
59
59
  def resource_import(klass)
@@ -264,13 +264,13 @@ class Class
264
264
  # * If the class argument is a Java class or a Java class name, then
265
265
  # the Ruby class is the JRuby wrapper for the Java class.
266
266
  #
267
- # @param [Class, String] the class or class name
267
+ # @param [Class, String] class_or_name the class or class name
268
268
  # @return [Class] the corresponding Ruby class
269
- def self.to_ruby(klass)
270
- case klass
271
- when Class then klass
272
- when String then Java.module_eval(klass)
273
- else to_ruby(klass.name)
269
+ def self.to_ruby(class_or_name)
270
+ case class_or_name
271
+ when Class then class_or_name
272
+ when String then eval to_ruby_name(class_or_name)
273
+ else to_ruby(class_or_name.name)
274
274
  end
275
275
  end
276
276
 
@@ -364,6 +364,18 @@ class Class
364
364
  private
365
365
 
366
366
  OBJ_INST_MTHDS = Object.instance_methods
367
+
368
+ # @param [String] jname the fully-qualified Java class or interface name
369
+ # @return [String] the JRuby class or module name
370
+ # #example
371
+ # Java.to_ruby_class_name('com.test.Sample') #=> Java::ComTest::Sample
372
+ def self.to_ruby_name(jname)
373
+ path = jname.split('.')
374
+ return "Java::#{jname}" if path.size == 1
375
+ cname = path[-1]
376
+ pkg = path[0...-1].map { |s| s.capitalize_first }.join
377
+ "Java::#{pkg}::#{cname}"
378
+ end
367
379
  end
368
380
 
369
381
  class Array
@@ -0,0 +1,15 @@
1
+ require 'json'
2
+
3
+ module CaRuby
4
+ module JSON
5
+ # JSON => {Resource} deserializer.
6
+ module Deserializer
7
+ # @param [String] json the JSON to deserialize
8
+ # @return [Resource] the deserialized object
9
+ def json_create(json)
10
+ # Make the new object from the json data attribute => value hash.
11
+ new(json['data'])
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module CaRuby
2
+ module JSON
3
+ # {Resource} => JSON serializer.
4
+ module Serializer
5
+ # @param args the JSON serialization options
6
+ # @return [String] the JSON representation of this {Resource}
7
+ def to_json(*args)
8
+ database.lazy_loader.disable do
9
+ # The JSON class must be scoped by the domain module, not the Java package, in order
10
+ # to recognize the Resource JSON hooks.
11
+ # The data is the attribute => value hash.
12
+ {'json_class' => [self.class.domain_module, self.class.name.demodulize].join('::'),
13
+ 'data' => value_hash(self.class.nonowner_attributes)
14
+ }.to_json(*args)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ module Enumerable
22
+ # @param args the JSON serialization options
23
+ # @return [String] the JSON representation of this Enumerable as an array
24
+ def to_json(*args)
25
+ to_a.to_json(*args)
26
+ end
27
+ end
@@ -84,16 +84,13 @@ module CaRuby
84
84
  def migrate(row, migrated)
85
85
  end
86
86
 
87
- # Returns whether this migration target domain object is valid. The default is true
88
- # if this domain object either has no owner or its owner is valid.
87
+ # Returns whether this migration target domain object is valid. The default is true.
89
88
  # A migration shim should override this method on the target if there are conditions
90
89
  # which determine whether the migration should be skipped for this target object.
91
90
  #
92
91
  # @return [Boolean] whether this migration target domain object is valid
93
92
  def migration_valid?
94
- # check that the owner is be valid
95
- ownr = owner
96
- ownr.nil? or ownr.migration_valid?
93
+ true
97
94
  end
98
95
 
99
96
  # Migrates this domain object's migratable references. This method is called by the
@@ -115,7 +112,7 @@ module CaRuby
115
112
  #
116
113
  # @param [{Symbol => Object}] row the input row field => value hash
117
114
  # @param [<Resource>] migrated the migrated instances, including this Resource
118
- # @param [{Symbol => String}] mth_hash a hash that associates this domain object's
115
+ # @param [{Symbol => String}, nil] mth_hash a hash that associates this domain object's
119
116
  # attributes to migration method names
120
117
  def migrate_references(row, migrated, mth_hash=nil)
121
118
  # migrate the owner
@@ -29,10 +29,11 @@ module CaRuby
29
29
  # @param [{Symbol => Object}] opts the migration options
30
30
  # @option opts [String] :database required application {Database}
31
31
  # @option opts [String] :target required target domain class
32
- # @option opts [String] :mapping required input field => caTissue attribute mapping file
33
- # @option opts [String] :defaults optional caTissue attribute => value default mapping file
32
+ # @option opts [<String>, String] :mapping required input field => caTissue attribute mapping file(s)
33
+ # @option opts [<String>, String] :defaults optional caTissue attribute => value default mapping file(s)
34
+ # @option opts [<String>, String] :filters optional caTissue attribute input value => caTissue value filter file(s)
34
35
  # @option opts [String] :input required source file to migrate
35
- # @option opts [String] :shims optional array of shim files to load
36
+ # @option opts [<String>, String] :shims optional shim file(s) to load
36
37
  # @option opts [String] :unique ensures that migrated objects which include the {Resource::Unique}
37
38
  # @option opts [String] :create optional flag indicating that existing target objects are ignored
38
39
  # @option opts [String] :bad optional invalid record file
@@ -74,8 +75,8 @@ module CaRuby
74
75
  alias :each :migrate
75
76
 
76
77
  private
77
-
78
- UNIQUIFY_SHIM = File.join(File.dirname(__FILE__), 'uniquify.rb')
78
+
79
+ REGEXP_PAT = /^\/(.*[^\\])\/([inx]+)?$/
79
80
 
80
81
  # Class {#migrate} with a {#save} block.
81
82
  def execute_save
@@ -106,10 +107,10 @@ module CaRuby
106
107
  end
107
108
 
108
109
  def parse_options(opts)
109
- logger.debug { "Migrator options: #{opts.qp}" }
110
- @fld_map_file = opts[:mapping]
111
- raise MigrationError.new("Migrator missing required field mapping file parameter") if @fld_map_file.nil?
112
- @def_file = opts[:defaults]
110
+ @fld_map_files = opts[:mapping]
111
+ raise MigrationError.new("Migrator missing required field mapping file parameter") if @fld_map_files.nil?
112
+ @def_files = opts[:defaults]
113
+ @filter_files = opts[:filters]
113
114
  @shims = opts[:shims] ||= []
114
115
  @offset = opts[:offset] ||= 0
115
116
  @input = Options.get(:input, opts)
@@ -136,14 +137,15 @@ module CaRuby
136
137
  raise MigrationError.new("No file to migrate") if @input.nil?
137
138
 
138
139
  # make a CSV loader which only converts input fields corresponding to non-String attributes
139
- logger.info("Migration input file: #{@input}.")
140
140
  @loader = CsvIO.new(@input, &method(:convert))
141
141
  logger.debug { "Migration data input file #{@input} headers: #{@loader.headers.qp}" }
142
142
 
143
143
  # create the class => path => default value hash
144
- @def_hash = @def_file ? load_defaults(@def_file) : LazyHash.new { Hash.new }
144
+ @def_hash = @def_files ? load_defaults_files(@def_files) : {}
145
+ # create the class => path => default value hash
146
+ @filter_hash = @filter_files ? load_filter_files(@filter_files) : {}
145
147
  # create the class => path => header hash
146
- fld_map = load_field_map(@fld_map_file)
148
+ fld_map = load_field_map_files(@fld_map_files)
147
149
  # create the class => paths hash
148
150
  @cls_paths_hash = create_class_paths_hash(fld_map, @def_hash)
149
151
  # create the path => class => header hash
@@ -208,30 +210,40 @@ module CaRuby
208
210
 
209
211
  # Adds missing klass owner classes to the migration class path hash (with empty paths).
210
212
  def add_owners(klass)
211
- klass.owners.each do |owner|
212
- next if @cls_paths_hash.detect_key { |other| other <= owner } or owner.abstract?
213
- logger.debug { "Migrator adding #{klass.qp} owner #{owner}" }
214
- @cls_paths_hash[owner] = Array::EMPTY_ARRAY
215
- add_owners(owner)
213
+ owner = missing_owner_for(klass) || return
214
+ logger.debug { "Migrator adding #{klass.qp} owner #{owner}" }
215
+ @cls_paths_hash[owner] = Array::EMPTY_ARRAY
216
+ add_owners(owner)
217
+ end
218
+
219
+ # @param [Class] klass the migration class
220
+ # @return [Class, nil] the missing class owner, if any
221
+ def missing_owner_for(klass)
222
+ # check for an owner among the current migration classes
223
+ return if klass.owners.any? do |owner|
224
+ @cls_paths_hash.detect_key { |other| other <= owner }
216
225
  end
226
+ # find the first non-abstract candidate owner
227
+ klass.owners.detect { |owner| not owner.abstract? }
217
228
  end
218
229
 
219
230
  # Creates the class => +migrate_+_<attribute>_ hash for the given klasses.
220
231
  def create_migration_method_hashes
221
- # the attribute metadata => migration method hash variable
222
- @attr_md_mgt_mth_map = {}
223
- # the class => attribute => migration method hash variable
224
- @mgt_mth_hash = {}
225
- # collect the migration methods
226
- customizable_class_attributes.each { |klass, attr_mds| add_migration_methods(klass, attr_mds) }
232
+ # the class => attribute => migration filter hash
233
+ @attr_flt_hash = {}
234
+ customizable_class_attributes.each do |klass, attr_mds|
235
+ flts = migration_filters(klass, attr_mds) || next
236
+ @attr_flt_hash[klass] = flts
237
+ end
238
+
227
239
  # print the migration shim methods
228
- unless @mgt_mth_hash.empty? then
240
+ unless @attr_flt_hash.empty? then
229
241
  printer_hash = LazyHash.new { Array.new }
230
- @mgt_mth_hash.each do |klass, attr_mth_hash|
231
- mthds = attr_mth_hash.values
232
- printer_hash[klass.qp] = mthds unless mthds.empty?
242
+ @attr_flt_hash.each do |klass, hash|
243
+ mths = hash.values.select { |flt| Symbol === flt }
244
+ printer_hash[klass.qp] = mths unless mths.empty?
233
245
  end
234
- logger.info("Migration shim methods: #{printer_hash.pp_s}.")
246
+ logger.info("Migration shim methods: #{printer_hash.pp_s}.") unless printer_hash.empty?
235
247
  end
236
248
  end
237
249
 
@@ -275,13 +287,43 @@ module CaRuby
275
287
  # Discovers methods of the form +migrate+__attribute_ implemented for the paths
276
288
  # in the given class => paths hash the given klass. The migrate method is called
277
289
  # on the input field value corresponding to the path.
278
- def add_migration_methods(klass, attr_mds)
290
+ def migration_filters(klass, attr_mds)
291
+ # the attribute => migration method hash
292
+ mth_hash = attribute_method_hash(klass, attr_mds)
293
+ proc_hash = attribute_proc_hash(klass, attr_mds)
294
+ return if mth_hash.empty? and proc_hash.empty?
295
+
296
+ # for each class path terminal attribute metadata, add the migration filters
297
+ # to the attribute metadata => filter hash
298
+ attr_mds.to_compact_hash do |attr_md|
299
+ # the filter proc
300
+ proc = proc_hash[attr_md.to_sym]
301
+ # the migration shim method
302
+ mth = mth_hash[attr_md.to_sym]
303
+ if mth then
304
+ if proc then
305
+ Proc.new do |obj, value, row|
306
+ # filter the value
307
+ fval = proc.call(value)
308
+ # call the migration method on the filtered value
309
+ obj.send(mth, fval, row) unless fval.nil?
310
+ end
311
+ else
312
+ # call the migration method
313
+ Proc.new { |obj, value, row| obj.send(mth, value, row) }
314
+ end
315
+ else
316
+ # call the filter
317
+ Proc.new { |obj, value, row| proc.call(value) }
318
+ end
319
+ end
320
+ end
321
+
322
+ def attribute_method_hash(klass, attr_mds)
279
323
  # the migrate methods, excluding the Migratable migrate_references method
280
324
  mths = klass.instance_methods(true).select { |mth| mth =~ /^migrate.(?!references)/ }
281
- return if mths.empty?
282
-
283
325
  # the attribute => migration method hash
284
- attr_mth_hash = {}
326
+ mth_hash = {}
285
327
  mths.each do |mth|
286
328
  # the attribute suffix, e.g. name for migrate_name or Name for migrateName
287
329
  suffix = /^migrate(_)?(.*)/.match(mth).captures[1]
@@ -290,24 +332,60 @@ module CaRuby
290
332
  # the attribute for the name, or skip if no such attribute
291
333
  attr = klass.standard_attribute(attr_nm) rescue next
292
334
  # associate the attribute => method
293
- attr_mth_hash[attr] = mth
335
+ mth_hash[attr] = mth
294
336
  end
295
-
296
- # for each class path terminal attribute metadata, add the migration methods
297
- # to the attribute metadata => migration method hash
337
+ mth_hash
338
+ end
339
+
340
+ # @return [Attribute => {Object => Object}] the filter migration methods
341
+ def attribute_proc_hash(klass, attr_mds)
342
+ hash = @filter_hash[klass]
343
+ if hash.nil? then return Hash::EMPTY_HASH end
344
+ proc_hash = {}
298
345
  attr_mds.each do |attr_md|
299
- # the attribute migration method
300
- mth = attr_mth_hash[attr_md.to_sym]
301
- # associate the Attribute => method
302
- @attr_md_mgt_mth_map[attr_md] ||= mth if mth
346
+ flt = hash[attr_md.to_sym] || next
347
+ proc_hash[attr_md.to_sym] = to_filter_proc(flt)
348
+ end
349
+ logger.debug { "Migration filters loaded for #{klass.qp} #{proc_hash.keys.to_series}." }
350
+ proc_hash
351
+ end
352
+
353
+ # @param [{Object => Object}] filter the config value mapping
354
+ # @return [Proc] the filter migration block
355
+ def to_filter_proc(filter)
356
+ # Split the filter into a straight value => value hash and a regexp => value hash.
357
+ ph, vh = filter.split { |k, v| k =~ REGEXP_PAT }
358
+ reh = {}
359
+ ph.each do |k, v|
360
+ pat, opt = REGEXP_PAT.match(k).captures
361
+ reopt = if opt then
362
+ case opt
363
+ when 'i' then Regexp::IGNORECASE
364
+ else raise MigrationError.new("Migration value filter regular expression #{k} qualifier not supported: expected 'i', found '#{opt}'")
365
+ end
366
+ end
367
+ re = Regexp.new(pat, reopt)
368
+ reh[re] = v.gsub(/\$\d/, '%s') if String === v
369
+ end
370
+ Proc.new do |value|
371
+ if vh.has_key?(value) then
372
+ vh[value]
373
+ else
374
+ # the first regex which matches the value
375
+ regexp = reh.detect_key { |re| value =~ re }
376
+ # If there is a match, then apply the filter to the match data.
377
+ # Otherwise, pass the value through unmodified.
378
+ regexp ? (reh[regexp] % $~.captures) : value
379
+ end
303
380
  end
304
- @mgt_mth_hash[klass] = attr_mth_hash
305
381
  end
306
382
 
307
- # loads the shim files.
383
+ # Loads the shim files.
384
+ #
385
+ # @param [<String>, String] files the file or file array
308
386
  def load_shims(files)
309
387
  logger.debug { "Loading shims with load path #{$:.pp_s}..." }
310
- files.each do |file|
388
+ files.enumerate do |file|
311
389
  # load the file
312
390
  begin
313
391
  require file
@@ -323,7 +401,7 @@ module CaRuby
323
401
  #
324
402
  # @yield (see #migrate)
325
403
  # @yieldparam (see #migrate)
326
- def migrate_rows # :yields: target
404
+ def migrate_rows
327
405
  # open an CSV output for bad records if the option is set
328
406
  if @bad_rec_file then
329
407
  @loader.trash = @bad_rec_file
@@ -359,7 +437,7 @@ module CaRuby
359
437
  mgt_cnt += 1
360
438
  # clear the migration state
361
439
  clear(target)
362
- else
440
+ else
363
441
  # If there is a bad file then warn, reject and continue. Otherwise, bail.
364
442
  if @bad_rec_file then
365
443
  logger.warn("Migration not performed on record #{rec_no}.")
@@ -394,32 +472,55 @@ module CaRuby
394
472
  #
395
473
  # @param [{Symbol => Object}] row the input row field => value hash
396
474
  # @return the migrated target object if the migration is valid, nil otherwise
397
- def migrate_row(row) # :yields: target
475
+ def migrate_row(row)
398
476
  # create an instance for each creatable class
399
477
  created = Set.new
400
478
  migrated = @creatable_classes.map { |klass| create(klass, row, created) }
401
479
  # migrate each object from the input row
402
480
  created.each { |obj| obj.migrate(row, migrated) }
403
- # remove invalid migrations
404
- valid, invalid = migrated.partition { |obj| migration_valid?(obj) }
405
- # set the references
406
- valid.each { |obj| obj.migrate_references(row, migrated, @mgt_mth_hash[obj.class]) }
481
+ valid = migrate_valid_references(row, migrated)
407
482
  # the target object
408
483
  target = valid.detect { |obj| @target_class === obj } || return
409
- # the target is invalid if it has an invalid owner
410
- return unless owner_valid?(target, valid, invalid)
411
484
  logger.debug { "Migrated target #{target}." }
412
485
  target
413
486
  end
414
487
 
488
+ # Sets the migration references for each valid migrated object.
489
+ #
490
+ # @param [Array] the migrated objects
491
+ # @return [Array] the valid migrated objects
492
+ def migrate_valid_references(row, migrated)
493
+ # Split the valid and invalid objects. The iteration is in reverse dependency order,
494
+ # since invalidating a dependent can invalidate the owner.
495
+ valid, invalid = migrated.transitive_closure(:dependents).reverse.partition do |obj|
496
+ if migration_valid?(obj) then
497
+ obj.migrate_references(row, migrated, @attr_flt_hash[obj.class])
498
+ true
499
+ else
500
+ obj.class.owner_attributes.each { |attr| obj.clear_attribute(attr) }
501
+ false
502
+ end
503
+ end
504
+
505
+ # Go back through the valid objects in dependency order to invalidate dependents
506
+ # whose owner is invalid.
507
+ valid.reverse.each do |obj|
508
+ unless owner_valid?(obj, valid, invalid) then
509
+ invalid << valid.delete(obj)
510
+ logger.debug { "Invalidated migrated #{obj} since it does not have a valid owner." }
511
+ end
512
+ end
513
+ valid
514
+ end
515
+
415
516
  # Returns whether the given domain object satisfies at least one of the following conditions:
416
- # * it has an owner among the valid objects
417
517
  # * it does not have an owner among the invalid objects
518
+ # * it has an owner among the valid objects
418
519
  #
419
520
  # @param [Resource] obj the domain object to check
420
521
  # @param [<Resource>] valid the valid migrated objects
421
522
  # @param [<Resource>] invalid the invalid migrated objects
422
- # @return [Boolean] whether the domain object is valid
523
+ # @return [Boolean] whether the owner is valid
423
524
  def owner_valid?(obj, valid, invalid)
424
525
  otypes = obj.class.owners
425
526
  invalid.all? { |other| not otypes.include?(other.class) } or
@@ -427,7 +528,7 @@ module CaRuby
427
528
  end
428
529
 
429
530
  # @param [Migratable] obj the migrated object
430
- # @return whether the migration is successful
531
+ # @return [Boolean] whether the migration is successful
431
532
  def migration_valid?(obj)
432
533
  if obj.migration_valid? then
433
534
  true
@@ -477,7 +578,8 @@ module CaRuby
477
578
  # @param row (see #create)
478
579
  # @param [<Resource>] created (see #create)
479
580
  def add_defaults(obj, row, created)
480
- @def_hash[obj.class].each do |path, value|
581
+ dh = @def_hash[obj.class] || return
582
+ dh.each do |path, value|
481
583
  # fill the reference path
482
584
  ref = fill_path(obj, path[0...-1], row, created)
483
585
  # set the attribute to the default value unless there is already a value
@@ -517,15 +619,19 @@ module CaRuby
517
619
  ref
518
620
  end
519
621
 
520
- # Sets the obj migratable Attribute attr_md to value from the given input row.
622
+ # Sets the given attribute value to the filtered input value. If there is a filter
623
+ # defined for the attribute, then that filter is applied. If there is a migration
624
+ # shim method with name +migrate_+_attribute_, then than method is called on the
625
+ # (possibly filtered) value. The target object attribute is set to the resulting
626
+ # filtered value.
627
+ #
628
+ # @param [Migratable] obj the target domain object
629
+ # @param [Attribute] attr_md the target attribute
630
+ # @param value the input value
631
+ # @param [{Symbol => Object}] row the input row
521
632
  def migrate_attribute(obj, attr_md, value, row)
522
- # a single value can be used for both a Numeric and a String attribute; coerce the value if necessary
523
633
  # if there is a shim migrate_<attribute> method, then call it on the input value
524
- mth = @attr_md_mgt_mth_map[attr_md]
525
- if mth and obj.respond_to?(mth) then
526
- value = obj.send(mth, value, row)
527
- return if value.nil?
528
- end
634
+ value = filter_value(obj, attr_md, value, row) || return
529
635
  # set the attribute
530
636
  begin
531
637
  obj.send(attr_md.writer, value)
@@ -534,6 +640,25 @@ module CaRuby
534
640
  end
535
641
  logger.debug { "Migrated #{obj.qp} #{attr_md} to #{value}." }
536
642
  end
643
+
644
+ # Calls the shim migrate_<attribute> method or config filter on the input value.
645
+ #
646
+ # @param value the input value
647
+ # @return the input value, if there is no filter, otherwise the filtered value
648
+ def filter_value(obj, attr_md, value, row)
649
+ filter = filter_for(obj, attr_md)
650
+ return value if filter.nil?
651
+ fval = filter.call(obj, value, row)
652
+ unless value == fval then
653
+ logger.debug { "Migration filter transformed #{obj.qp} #{attr_md} value from #{value.qp} to #{fval.qp}." }
654
+ end
655
+ fval
656
+ end
657
+
658
+ def filter_for(obj, attr_md)
659
+ flts = @attr_flt_hash[obj.class] || return
660
+ flts[attr_md]
661
+ end
537
662
 
538
663
  # @param [Resource] obj the domain object to save in the database
539
664
  # @return [Resource, nil] obj if the save is successful, nil otherwise
@@ -557,30 +682,12 @@ module CaRuby
557
682
  @rec_cnt + 1
558
683
  end
559
684
 
560
- # @param [String] file the migration fields configuration file
685
+ # @param [<String>, String] files the migration fields mapping file or file array
561
686
  # @return [{Class => {Attribute => Symbol}}] the class => path => header hash
562
- # loaded from the configuration file
563
- def load_field_map(file)
564
- # load the field mapping config file
565
- begin
566
- config = YAML::load_file(file)
567
- rescue
568
- raise MigrationError.new("Could not read field map file #{file}: " + $!)
569
- end
570
-
571
- # collect the class => path => header entries
687
+ # loaded from the mapping files
688
+ def load_field_map_files(files)
572
689
  map = LazyHash.new { Hash.new }
573
- config.each do |field, attr_list|
574
- next if attr_list.blank?
575
- # the header accessor method for the field
576
- header = @loader.accessor(field)
577
- raise MigrationError.new("Field defined in migration configuration #{file} not found in input file #{@input} headers: #{field}") if header.nil?
578
- # associate each attribute path in the property value with the header
579
- attr_list.split(/,\s*/).each do |path_s|
580
- klass, path = create_attribute_path(path_s)
581
- map[klass][path] = header
582
- end
583
- end
690
+ files.enumerate { |file| load_field_map_file(file, map) }
584
691
 
585
692
  # include the target class
586
693
  map[@target_class] ||= Hash.new
@@ -602,26 +709,95 @@ module CaRuby
602
709
  map.delete_if do |klass, paths|
603
710
  klass.abstract? or classes.any? { |other| other < klass }
604
711
  end
712
+
605
713
  map
606
714
  end
607
715
 
608
- def load_defaults(file)
716
+ # @param [String] file the migration fields configuration file
717
+ # @param [{Class => {Attribute => Symbol}}] hash the class => path => header hash
718
+ # loaded from the configuration file
719
+ def load_field_map_file(file, hash)
609
720
  # load the field mapping config file
610
721
  begin
611
722
  config = YAML::load_file(file)
612
723
  rescue
613
- raise MigrationError.new("Could not read defaults file #{file}: " + $!)
724
+ raise MigrationError.new("Could not read field map file #{file}: " + $!)
614
725
  end
615
726
 
727
+ # collect the class => path => header entries
728
+ config.each do |field, attr_list|
729
+ next if attr_list.blank?
730
+ # the header accessor method for the field
731
+ header = @loader.accessor(field)
732
+ raise MigrationError.new("Field defined in migration configuration #{file} not found in input file #{@input} headers: #{field}") if header.nil?
733
+ # associate each attribute path in the property value with the header
734
+ attr_list.split(/,\s*/).each do |path_s|
735
+ klass, path = create_attribute_path(path_s)
736
+ hash[klass][path] = header
737
+ end
738
+ end
739
+ end
740
+
741
+ # Loads the defaults config files.
742
+ #
743
+ # @param [<String>, String] files the file or file array to load
744
+ # @return [<Class => <String => Object>>] the class => path => default value entries
745
+ def load_defaults_files(files)
746
+ # collect the class => path => value entries from each defaults file
747
+ hash = LazyHash.new { Hash.new }
748
+ files.enumerate { |file| load_defaults_file(file, hash) }
749
+ hash
750
+ end
751
+
752
+ # Loads the defaults config file into the given hash.
753
+ #
754
+ # @param [String] file the file to load
755
+ # @param [<Class => <String => Object>>] hash the class => path => default value entries
756
+ def load_defaults_file(file, hash)
757
+ begin
758
+ config = YAML::load_file(file)
759
+ rescue
760
+ raise MigrationError.new("Could not read defaults file #{file}: " + $!)
761
+ end
616
762
  # collect the class => path => value entries
617
- map = LazyHash.new { Hash.new }
618
763
  config.each do |path_s, value|
619
764
  next if value.nil_or_empty?
620
765
  klass, path = create_attribute_path(path_s)
621
- map[klass][path] = value
766
+ hash[klass][path] = value
767
+ end
768
+ end
769
+ # Loads the filter config files.
770
+ #
771
+ # @param [<String>, String] files the file or file array to load
772
+ # @return [<Class => <String => Object>>] the class => path => default value entries
773
+ def load_filter_files(files)
774
+ # collect the class => path => value entries from each defaults file
775
+ hash = {}
776
+ files.enumerate { |file| load_filter_file(file, hash) }
777
+ hash
778
+ end
779
+
780
+ # Loads the filter config file into the given hash.
781
+ #
782
+ # @param [String] file the file to load
783
+ # @param [<Class => <String => <Object => Object>>>] hash the class => path => input value => caTissue value entries
784
+ def load_filter_file(file, hash)
785
+ begin
786
+ config = YAML::load_file(file)
787
+ rescue
788
+ raise MigrationError.new("Could not read filter file #{file}: " + $!)
789
+ end
790
+ # collect the class => attribute => filter entries
791
+ config.each do |path_s, flt|
792
+ next if flt.nil_or_empty?
793
+ klass, path = create_attribute_path(path_s)
794
+ unless path.size == 1 then
795
+ raise MigrationError.new("Migration filter configuration path not supported: #{path_s}")
796
+ end
797
+ attr = klass.standard_attribute(path.first.to_sym)
798
+ flt_hash = hash[klass] ||= {}
799
+ flt_hash[attr] = flt
622
800
  end
623
-
624
- map
625
801
  end
626
802
 
627
803
  # @param [String] path_s a period-delimited path string path_s in the form _class_(._attribute_)+