batch-kit 0.3

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +165 -0
  4. data/lib/batch-kit.rb +9 -0
  5. data/lib/batch-kit/arguments.rb +57 -0
  6. data/lib/batch-kit/config.rb +517 -0
  7. data/lib/batch-kit/configurable.rb +68 -0
  8. data/lib/batch-kit/core_ext/enumerable.rb +97 -0
  9. data/lib/batch-kit/core_ext/file.rb +69 -0
  10. data/lib/batch-kit/core_ext/file_utils.rb +103 -0
  11. data/lib/batch-kit/core_ext/hash.rb +17 -0
  12. data/lib/batch-kit/core_ext/numeric.rb +17 -0
  13. data/lib/batch-kit/core_ext/string.rb +88 -0
  14. data/lib/batch-kit/database.rb +133 -0
  15. data/lib/batch-kit/database/java_util_log_handler.rb +65 -0
  16. data/lib/batch-kit/database/log4r_outputter.rb +57 -0
  17. data/lib/batch-kit/database/models.rb +548 -0
  18. data/lib/batch-kit/database/schema.rb +229 -0
  19. data/lib/batch-kit/encryption.rb +7 -0
  20. data/lib/batch-kit/encryption/java_encryption.rb +178 -0
  21. data/lib/batch-kit/encryption/ruby_encryption.rb +175 -0
  22. data/lib/batch-kit/events.rb +157 -0
  23. data/lib/batch-kit/framework/acts_as_job.rb +197 -0
  24. data/lib/batch-kit/framework/acts_as_sequence.rb +123 -0
  25. data/lib/batch-kit/framework/definable.rb +169 -0
  26. data/lib/batch-kit/framework/job.rb +121 -0
  27. data/lib/batch-kit/framework/job_definition.rb +105 -0
  28. data/lib/batch-kit/framework/job_run.rb +145 -0
  29. data/lib/batch-kit/framework/runnable.rb +235 -0
  30. data/lib/batch-kit/framework/sequence.rb +87 -0
  31. data/lib/batch-kit/framework/sequence_definition.rb +38 -0
  32. data/lib/batch-kit/framework/sequence_run.rb +48 -0
  33. data/lib/batch-kit/framework/task_definition.rb +89 -0
  34. data/lib/batch-kit/framework/task_run.rb +53 -0
  35. data/lib/batch-kit/helpers/date_time.rb +54 -0
  36. data/lib/batch-kit/helpers/email.rb +198 -0
  37. data/lib/batch-kit/helpers/html.rb +175 -0
  38. data/lib/batch-kit/helpers/process.rb +101 -0
  39. data/lib/batch-kit/helpers/zip.rb +30 -0
  40. data/lib/batch-kit/job.rb +11 -0
  41. data/lib/batch-kit/lockable.rb +138 -0
  42. data/lib/batch-kit/loggable.rb +78 -0
  43. data/lib/batch-kit/logging.rb +169 -0
  44. data/lib/batch-kit/logging/java_util_logger.rb +87 -0
  45. data/lib/batch-kit/logging/log4r_logger.rb +71 -0
  46. data/lib/batch-kit/logging/null_logger.rb +35 -0
  47. data/lib/batch-kit/logging/stdout_logger.rb +96 -0
  48. data/lib/batch-kit/resources.rb +191 -0
  49. data/lib/batch-kit/sequence.rb +7 -0
  50. metadata +122 -0
@@ -0,0 +1,68 @@
1
+ require_relative 'config'
2
+
3
+
4
+ class BatchKit
5
+
6
+ # Adds a configure class method that can be used to load a configuration file
7
+ # and make it available to the class and instances of it.
8
+ module Configurable
9
+
10
+ # Defines the methods that are to be added as class methods to the class
11
+ # that includes the Configurable module.
12
+ module ClassMethods
13
+
14
+ # Configure the class by loading the configuration files specifed.
15
+ # If the last argument passed to this method is a Hash, it is
16
+ # treated an options hash, which is passed into {BatchKit::Config}.
17
+ #
18
+ # @param cfg_files [Array<String>] Path(s) to the configuration
19
+ # file(s) to be loaded into a single {BatchKit::Config} object.
20
+ # @option cfg_files [String] :decryption_key The master key for
21
+ # decrypting any encrypted values in the configuration files.
22
+ def configure(*cfg_files)
23
+ options = cfg_files.last.is_a?(Hash) ? cfg_files.pop.clone : {}
24
+ if defined?(BatchKit::Events)
25
+ Events.publish(self, 'config.pre-load', config, cfg_files)
26
+ end
27
+ config.decryption_key = options.delete(:decryption_key) if options[:decryption_key]
28
+ config.merge!(options)
29
+ cfg_files.each do |cfg_file|
30
+ config.load(cfg_file, options)
31
+ end
32
+ if defined?(BatchKit::Events)
33
+ Events.publish(self, 'config.post-load', config)
34
+ end
35
+ config
36
+ end
37
+
38
+
39
+ # Returns the {BatchKit::Config} object produced when loading the
40
+ # configuration files (or creates a new instance if no files were
41
+ # loaded).
42
+ def config
43
+ @config ||= Config.new
44
+ end
45
+
46
+ end
47
+
48
+
49
+ # Used to extend the including class with the class methods defined in
50
+ # {ClassMethods}.
51
+ def self.included(base)
52
+ base.extend(ClassMethods)
53
+ end
54
+
55
+
56
+ # Each object instance gets its own copy of the class configuration, so
57
+ # that any modifications they make are local to the object instance.
58
+ #
59
+ # @return [BatchKit::Config] a copy of the class configuration specific
60
+ # to this instance.
61
+ def config
62
+ @config ||= self.class.config.clone
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
@@ -0,0 +1,97 @@
1
+ require 'thread'
2
+
3
+
4
+ # Re-opens the Ruby standard Enumerable module to add some additional utility
5
+ # methods to all enumerables.
6
+ module Enumerable
7
+
8
+ # Maps each item in the Enumerable, surrounding it with the +left+ and +right+
9
+ # strings. If +right+ is not specified, it is set to the same string as +left+,
10
+ # which in turn is defaulted to the double-quote character.
11
+ #
12
+ # @param left [String] The character to precede each item with.
13
+ # @param right [String] The character to follow each item with.
14
+ def surround(left = '"', right = left)
15
+ self.map{ |item| "#{left}#{item}#{right}" }
16
+ end
17
+
18
+
19
+ # Surrounds each item in the Enumerable with single quotes.
20
+ def squote
21
+ self.surround("'")
22
+ end
23
+
24
+
25
+ # Surrounds each item in the Enumerable with double quotes.
26
+ def dquote
27
+ self.surround('"')
28
+ end
29
+
30
+
31
+ # Convenience function for spawning multiple threads to do a common task,
32
+ # driven by the contents of this enumerable. Each entry in self will be
33
+ # be yielded to a new thread, which will then call the supplied block with
34
+ # the element.
35
+ #
36
+ # @param options [Hash] An options hash.
37
+ # @option options [Boolean] :abort_on_exception If true, and any thread
38
+ # throws an exception during processing, abort processing of the
39
+ # collection immediately.
40
+ # @option options [Integer] :threads The number of threads to use to
41
+ # process the collection. Default is no more than 4 (less if the
42
+ # collection contains fewer than 4 items).
43
+ def concurrent_each(options = {}, &blk)
44
+ if self.count < 2
45
+ self.each(&blk)
46
+ else
47
+ abort_opt = options.fetch(:abort_on_exception, true)
48
+ Thread.abort_on_exception = abort_opt
49
+
50
+ # Push items onto a queue from which work items can be removed by
51
+ # threads in the pool
52
+ queue = Queue.new
53
+ self.each{ |it| queue << it }
54
+
55
+ # Setup thread pool to iterate over work queue
56
+ thread_count = options.fetch(:threads, [4, self.count].min)
57
+ threads = []
58
+
59
+ # Launch each worker thread, which loops extracting work items from
60
+ # the queue until it is empty
61
+ (0...thread_count).each do |i|
62
+ threads << Thread.new do
63
+ begin
64
+ while work_item = queue.pop(true)
65
+ if abort_opt
66
+ # Raise exception on main thread
67
+ begin
68
+ yield work_item
69
+ rescue Exception => ex
70
+ Thread.main.raise ex
71
+ end
72
+ else
73
+ # Exceptions will be picked up below when main thread joins
74
+ yield work_item
75
+ end
76
+ end
77
+ rescue ThreadError
78
+ # Work queue is empty, so exit loop
79
+ end
80
+ end
81
+ end
82
+
83
+ # Now wait for all threads in pool to complete
84
+ ex = nil
85
+ threads.each do |th|
86
+ begin
87
+ th.join
88
+ rescue Exception => t
89
+ ex = t unless ex
90
+ end
91
+ end
92
+ raise ex if ex
93
+ end
94
+ end
95
+
96
+ end
97
+
@@ -0,0 +1,69 @@
1
+ class BatchKit
2
+
3
+ module FileExtensions
4
+
5
+ module ClassMethods
6
+
7
+ # Return just the name (without any extension) of a path.
8
+ def nameonly(path)
9
+ File.basename(path, File.extname(path))
10
+ end
11
+
12
+ end
13
+
14
+
15
+ def self.included(cls)
16
+ cls.extend(ClassMethods)
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
23
+
24
+ File.class_eval do
25
+
26
+ include BatchKit::FileExtensions
27
+
28
+ class << self
29
+
30
+ unless method_defined?(:open_without_bom)
31
+
32
+ # Add support for writing a BOM if the +mode_string+ includes 'bom',
33
+ # and the mode is write.
34
+ alias_method :open_without_bom, :open
35
+ def open(*args, &blk)
36
+ if args.length >= 2 && (mode_string = args[1]).is_a?(String) &&
37
+ mode_string =~ /^(w|a):(.*)bom/i
38
+ write_mode = $1.downcase == 'w'
39
+ args[1] = mode_string.sub(/bom\||[\-|]bom/, '')
40
+ f = open_without_bom(*args)
41
+ bom_hex = case mode_string
42
+ when /utf-?8/i
43
+ "\xEF\xBB\xBF"
44
+ when /utf-16be/i
45
+ "\xFE\xFF"
46
+ when /utf-16le/i
47
+ "\xFF\xFE"
48
+ when /utf-32be/i
49
+ "\x00\x00\xFE\xFF"
50
+ when /utf-32le/i
51
+ "\xFE\xFF\x00\x00"
52
+ end
53
+ f << bom_hex.force_encoding(f.external_encoding) if write_mode
54
+ if block_given?
55
+ yield f
56
+ f.close
57
+ else
58
+ f
59
+ end
60
+ else
61
+ open_without_bom(*args, &blk)
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
69
+ end
@@ -0,0 +1,103 @@
1
+ require 'fileutils'
2
+ require 'date'
3
+
4
+
5
+ # Re-opens the FileUtils module in the Ruby standard library to add new
6
+ # functionality.
7
+ module FileUtils
8
+
9
+ # Set the default archive directory for use on subsequent calls to #archive.
10
+ attr_accessor :archive_dir
11
+ module_function :archive_dir, :archive_dir=
12
+
13
+
14
+ # Archive existing files at +paths+, where +paths+ is one or more
15
+ # Dir#glob patterns. Archiving consists of renaming files to include a
16
+ # timestamp in the file name. This permits multiple copies of the same
17
+ # file to exist in a single directory. The timestamp is created from
18
+ # the last modification date of the file being archived.
19
+ #
20
+ # The last argument passed may be an options Hash, which can be used
21
+ # to change the default behaviour. Valid options are:
22
+ # @param paths [Array<String|Hash>] A list of paths to be archived. If
23
+ # the last item passed to the method is a Hahs, it is treated as an
24
+ # an options hash.
25
+ # @option paths [String] :archive_dir The directory in which to place
26
+ # archived files. If not specified, archived files are placed in the
27
+ # same directory.
28
+ # @option paths [Fixnum] :archive_days The number of days for which to
29
+ # keep archived files. Defaults to nil, meaning there is no maximum
30
+ # number of days.
31
+ # @option paths [Fixnum] :archive_copies The maximum number of copies
32
+ # to keep of an archived file. Defaults to 10.
33
+ def archive(*paths)
34
+ if paths.last.is_a?(Hash)
35
+ options = paths.pop
36
+ else
37
+ options = {archive_copies: 10}
38
+ end
39
+ archive_dir = options[:archive_dir] || @archive_dir
40
+ archive_copies = options[:archive_copies]
41
+ if archive_copies && 1 > archive_copies
42
+ raise ArgumentError, ":archive_copies option must be positive"
43
+ end
44
+ archive_days = options[:archive_days]
45
+ if archive_days && 1 > archive_days
46
+ raise ArgumentError, ":archive_days option must be positive"
47
+ end
48
+ cutoff_date = archive_days && (Date.today - archive_days)
49
+
50
+ FileUtils.mkdir_p(archive_dir) rescue nil if archive_dir
51
+
52
+ # Create archives of files that match the patterns in +paths+
53
+ archive_count = 0
54
+ Dir[*paths].each do |file_name|
55
+ next if file_name =~ /\d{8}.\d{6}(?:\.[^.]+)?$/
56
+ File.rename(file_name, '%s/%s.%s%s' % [
57
+ archive_dir || File.dirname(file_name),
58
+ File.basename(file_name, File.extname(file_name)),
59
+ File.mtime(file_name).strftime('%Y%m%d.%H%M%S'),
60
+ File.extname(file_name)
61
+ ]) rescue next
62
+ archive_count += 1
63
+ end
64
+
65
+ if archive_copies || cutoff_date
66
+ # Find all copies of each unique file matching +paths+
67
+ purge_sets = Hash.new{ |h, k| h[k] = [] }
68
+ folders = archive_dir ? [archive_dir] : paths.map{ |path| File.dirname(path) }.uniq
69
+ folders.each do |folder|
70
+ Dir["#{folder}/*.????????.??????.*"].each do |path|
71
+ if path =~ /^(.+)\.\d{8}\.\d{6}(\.[^.]+)?$/
72
+ purge_sets["#{$1}#{$2}"] << path
73
+ end
74
+ end
75
+ end
76
+
77
+ # Now purge the oldest archives, such that we keep a maximum of
78
+ # +archive_copies+, and no file older than +cutoff_date+.
79
+ purge_sets.each do |orig_name, old_files|
80
+ old_files.sort!
81
+ old_size = old_files.size
82
+ purge_files = []
83
+ if archive_copies && old_size > archive_copies
84
+ purge_files = old_files.slice!(0, old_size - archive_copies)
85
+ end
86
+ if cutoff_date
87
+ vold_files = old_files.reject! do |path|
88
+ path =~ /(\d{4})(\d{2})(\d{2})\.\d{6}(?:\.[^.]+)?$/
89
+ file_date = Date.new($1.to_i, $2.to_i, $3.to_i)
90
+ file_date >= cutoff_date
91
+ end
92
+ purge_files.concat(vold_files) if vold_files
93
+ end
94
+ if purge_files.size > 0
95
+ FileUtils.rm_f(purge_files) rescue nil
96
+ end
97
+ end
98
+ end
99
+ archive_count
100
+ end
101
+ module_function :archive
102
+
103
+ end
@@ -0,0 +1,17 @@
1
+ class BatchKit
2
+
3
+ module HashExtensions
4
+
5
+ # Converts a Hash object to a BatchKit::Config object
6
+ def to_cfg
7
+ self.is_a?(BatchKit::Config) ? self : BatchKit::Config.new(self)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+
14
+
15
+ Hash.class_eval do
16
+ include BatchKit::HashExtensions
17
+ end
@@ -0,0 +1,17 @@
1
+ class BatchKit
2
+
3
+ module NumericExtensions
4
+
5
+ # Converts an integer to a comma-separated string, e.g. 1024 becomes "1,024"
6
+ def with_commas
7
+ self.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, "\\1,")
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+
14
+
15
+ Numeric.class_eval do
16
+ include BatchKit::NumericExtensions
17
+ end
@@ -0,0 +1,88 @@
1
+ class BatchKit
2
+
3
+ module StringExtensions
4
+
5
+ unless String.instance_methods.include?(:titleize)
6
+
7
+ # A very simple #titleize method to capitalise the first letter of
8
+ # each word, and replace underscores with spaces. Nowhere near as
9
+ # powerful as ActiveSupport#titleize, so we only define it if no
10
+ # existing #titleize method is found on String.
11
+ def titleize
12
+ self.gsub(/_/, ' ').gsub(/\b([a-z])/){ $1.upcase }
13
+ end
14
+
15
+ end
16
+
17
+
18
+ # Wraps the text in +self+ to lines no longer than +width+.
19
+ #
20
+ # The algorithm uses the following variables:
21
+ # - end_pos is the last non-space character in self
22
+ # - start is the position in self of the start of the next line to be
23
+ # processed.
24
+ # - nl_pos is the location of the next new-line character after start.
25
+ # - ws_pos is the location of the last space character between start and
26
+ # start + width.
27
+ # - wb_pos is the location of the last word-break character between start
28
+ # and start + width - 1.
29
+ #
30
+ # @param width [Fixnum] The maximum number of characters in each line.
31
+ def wrap_text(width)
32
+ if width > 0 && (self.length > width || self.index("\n"))
33
+ lines = []
34
+ start, nl_pos, ws_pos, wb_pos, end_pos = 0, 0, 0, 0, self.rindex(/[^\s]/)
35
+ while start < end_pos
36
+ last_start = start
37
+ nl_pos = self.index("\n", start)
38
+ ws_pos = self.rindex(/ +/, start + width)
39
+ wb_pos = self.rindex(/[\-,.;#)}\]\/\\]/, start + width - 1)
40
+ ### Debug code ###
41
+ #STDERR.puts self
42
+ #ind = ' ' * end_pos
43
+ #ind[start] = '('
44
+ #ind[start+width < end_pos ? start+width : end_pos] = ']'
45
+ #ind[nl_pos] = 'n' if nl_pos
46
+ #ind[wb_pos] = 'b' if wb_pos
47
+ #ind[ws_pos] = 's' if ws_pos
48
+ #STDERR.puts ind
49
+ ### End debug code ###
50
+ if nl_pos && nl_pos <= start + width
51
+ lines << self[start...nl_pos].strip
52
+ start = nl_pos + 1
53
+ elsif end_pos < start + width
54
+ lines << self[start..end_pos]
55
+ start = end_pos
56
+ elsif ws_pos && ws_pos > start && ((wb_pos.nil? || ws_pos > wb_pos) ||
57
+ (wb_pos && wb_pos > 5 && wb_pos - 5 < ws_pos))
58
+ lines << self[start...ws_pos]
59
+ start = self.index(/[^\s]/, ws_pos + 1)
60
+ elsif wb_pos && wb_pos > start
61
+ lines << self[start..wb_pos]
62
+ start = wb_pos + 1
63
+ else
64
+ lines << self[start...(start+width)]
65
+ start += width
66
+ end
67
+ if start <= last_start
68
+ # Detect an infinite loop, and just return the original text
69
+ STDERR.puts "Inifinite loop detected at #{__FILE__}:#{__LINE__}"
70
+ STDERR.puts " width: #{width}, start: #{start}, nl_pos: #{nl_pos}, " +
71
+ "ws_pos: #{ws_pos}, wb_pos: #{wb_pos}"
72
+ return [self]
73
+ end
74
+ end
75
+ lines
76
+ else
77
+ [self]
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+
86
+ String.class_eval do
87
+ include BatchKit::StringExtensions
88
+ end