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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +165 -0
- data/lib/batch-kit.rb +9 -0
- data/lib/batch-kit/arguments.rb +57 -0
- data/lib/batch-kit/config.rb +517 -0
- data/lib/batch-kit/configurable.rb +68 -0
- data/lib/batch-kit/core_ext/enumerable.rb +97 -0
- data/lib/batch-kit/core_ext/file.rb +69 -0
- data/lib/batch-kit/core_ext/file_utils.rb +103 -0
- data/lib/batch-kit/core_ext/hash.rb +17 -0
- data/lib/batch-kit/core_ext/numeric.rb +17 -0
- data/lib/batch-kit/core_ext/string.rb +88 -0
- data/lib/batch-kit/database.rb +133 -0
- data/lib/batch-kit/database/java_util_log_handler.rb +65 -0
- data/lib/batch-kit/database/log4r_outputter.rb +57 -0
- data/lib/batch-kit/database/models.rb +548 -0
- data/lib/batch-kit/database/schema.rb +229 -0
- data/lib/batch-kit/encryption.rb +7 -0
- data/lib/batch-kit/encryption/java_encryption.rb +178 -0
- data/lib/batch-kit/encryption/ruby_encryption.rb +175 -0
- data/lib/batch-kit/events.rb +157 -0
- data/lib/batch-kit/framework/acts_as_job.rb +197 -0
- data/lib/batch-kit/framework/acts_as_sequence.rb +123 -0
- data/lib/batch-kit/framework/definable.rb +169 -0
- data/lib/batch-kit/framework/job.rb +121 -0
- data/lib/batch-kit/framework/job_definition.rb +105 -0
- data/lib/batch-kit/framework/job_run.rb +145 -0
- data/lib/batch-kit/framework/runnable.rb +235 -0
- data/lib/batch-kit/framework/sequence.rb +87 -0
- data/lib/batch-kit/framework/sequence_definition.rb +38 -0
- data/lib/batch-kit/framework/sequence_run.rb +48 -0
- data/lib/batch-kit/framework/task_definition.rb +89 -0
- data/lib/batch-kit/framework/task_run.rb +53 -0
- data/lib/batch-kit/helpers/date_time.rb +54 -0
- data/lib/batch-kit/helpers/email.rb +198 -0
- data/lib/batch-kit/helpers/html.rb +175 -0
- data/lib/batch-kit/helpers/process.rb +101 -0
- data/lib/batch-kit/helpers/zip.rb +30 -0
- data/lib/batch-kit/job.rb +11 -0
- data/lib/batch-kit/lockable.rb +138 -0
- data/lib/batch-kit/loggable.rb +78 -0
- data/lib/batch-kit/logging.rb +169 -0
- data/lib/batch-kit/logging/java_util_logger.rb +87 -0
- data/lib/batch-kit/logging/log4r_logger.rb +71 -0
- data/lib/batch-kit/logging/null_logger.rb +35 -0
- data/lib/batch-kit/logging/stdout_logger.rb +96 -0
- data/lib/batch-kit/resources.rb +191 -0
- data/lib/batch-kit/sequence.rb +7 -0
- 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
|