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