batch-kit 0.3

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