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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a3d2112789225de19ecd9118d3cec6a9d5b88798
|
4
|
+
data.tar.gz: 9e3df2077d4dd002c2238dc097643ed95258938c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 78c81cba988369e2f053885f7fd8c3eebbfb963b8c6860e15241dc557ba39578bd84411936364b7ff5ea3206cd1b2827393b2ac0a70e5c7d909dc5002a3c9433
|
7
|
+
data.tar.gz: a77407066d3b7168ccc1c9af9cb0ffdc090bd16210dec372eac58ad9b615046ca4f9c94f2f5e139ae8036975142ac25a4147704fd2afb775fe29aa445544723a
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013, Adam Gardiner
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
14
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
15
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
17
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
18
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
19
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
20
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
21
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
22
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# Batch Kit
|
2
|
+
|
3
|
+
Batch Kit is a framework for creating batch jobs, i.e. small utility programs
|
4
|
+
that are often run in batch via a scheduler to perform some kind of repetitive
|
5
|
+
task. Using batch-kit, you can develop batch jobs from Ruby programs that are
|
6
|
+
simple to write, but which are:
|
7
|
+
|
8
|
+
- __Robust__: Batch jobs handle any uncaught exceptions gracefully, ensuring the
|
9
|
+
exception is reported, resources are freed, and the job itself exits with a
|
10
|
+
non-zero error code.
|
11
|
+
- __Configurable__: Batch Kit jobs make it easy to define the command-line
|
12
|
+
arguments the job can take, and also make it simple to use configuration files
|
13
|
+
in either property or YAML format.
|
14
|
+
- __Secure__: Configuration files support encryption of sensitive items such as
|
15
|
+
passwords.
|
16
|
+
- __Measured__: Batch jobs can use a database to gather statistics of runs, such
|
17
|
+
as the number of runs of each job, the average, minimum, and maximum duration,
|
18
|
+
arguments passed to the job, log output, etc.
|
19
|
+
|
20
|
+
To provide these capabilities, the batch kit framework provides:
|
21
|
+
|
22
|
+
- A job framework, which can be used to turn any class into a batch job. This
|
23
|
+
can be done either by extending the {BatchKit::Job} class, or by including the
|
24
|
+
{BatchKit::ActsAsJob} module. The job framework allows for new or existing job
|
25
|
+
and task methods to be created. Both job and task methods add
|
26
|
+
{https://en.wikipedia.org/wiki/Advice_(programming) advices} that wrap
|
27
|
+
the logic of the method with exception handlers, as well as gathering
|
28
|
+
statistics about the status and duration of the task or job. These can be
|
29
|
+
persisted to a database for job reporting.
|
30
|
+
|
31
|
+
- A facade over the Log4r and java.util.logging log frameworks, allowing
|
32
|
+
sophisticated logging with colour output to the console, and persistent
|
33
|
+
output to log files and to a database.
|
34
|
+
|
35
|
+
- A configuration class ({BatchKit::Config}), which supports either property or
|
36
|
+
YAML-based configuration files. The {BatchKit::Config} class extends Hash,
|
37
|
+
providing:
|
38
|
+
|
39
|
+
+ __Flexible Access__: keys are case-insensitive, can be accessed using
|
40
|
+
either strings or symbols, and can be accessed using BatchKit::Config#[]
|
41
|
+
or accessor-style methods
|
42
|
+
+ __Support for Placeholder Variables__: substitution variables can be used
|
43
|
+
in the configuration file, and will be expanded either from higher-level
|
44
|
+
properties in the configuration tree, or left to be resolved at a later
|
45
|
+
time.
|
46
|
+
+ __Encryption__: any property value can be encrypted, and will be stored in
|
47
|
+
memory in encrypted form, and only be decrypted when accessed explicitly.
|
48
|
+
Encryption is performed using AES-128 bit encryption via a separate
|
49
|
+
master key (which should not be stored in the same configuration file).
|
50
|
+
|
51
|
+
- A resource manager class that can be used to ensure the cleanup of any
|
52
|
+
resource that has an acquire/release pair of methods. Use of the
|
53
|
+
{BatchiKit::ResourceManager} class ensures that a job releases all resources
|
54
|
+
in both success and error outcomes. Support is also provided for locking
|
55
|
+
resources, such that concurrent or discrete jobs that share a resource can
|
56
|
+
coordinate their use.
|
57
|
+
|
58
|
+
- Helpers: helper modules are provided for common batch tasks, such as zipping
|
59
|
+
files, sending emails, archiving files etc.
|
60
|
+
|
61
|
+
## Example Usage
|
62
|
+
|
63
|
+
The simplest way to use the batch kit framework is to create a class for your job
|
64
|
+
that extends the {BatchKit::Job} class.
|
65
|
+
|
66
|
+
```
|
67
|
+
require 'batch-kit/job'
|
68
|
+
|
69
|
+
class MyJob < BatchKit::Job
|
70
|
+
```
|
71
|
+
|
72
|
+
Next, use the {BatchKit::Configurable::ClassMethods#configure configure} method
|
73
|
+
to add any configuration file(s) your job needs to read to load configuration
|
74
|
+
settings that control its behaviour:
|
75
|
+
|
76
|
+
```
|
77
|
+
configure 'my_config.yaml'
|
78
|
+
```
|
79
|
+
|
80
|
+
The job configuration is now available from both the class itself, and instances
|
81
|
+
of the class, via the {BatchKit::Job.config #config} method. Making the
|
82
|
+
configuration available from the class allows it to be used while defining the
|
83
|
+
class, e.g. when defining default values for command-line arguments your job
|
84
|
+
provides.
|
85
|
+
|
86
|
+
Command-line arguments are supported in batch kit jobs via the use of the
|
87
|
+
{https://github.com/agardiner/arg-parser arg-parser} gem. This provides a
|
88
|
+
DSL for defining various different kinds of command-line arguments:
|
89
|
+
|
90
|
+
```
|
91
|
+
positional_arg :spec, 'A path to a specification file',
|
92
|
+
default: config.default_spec
|
93
|
+
flag_arg :verbose, 'Output more details during processing'
|
94
|
+
```
|
95
|
+
|
96
|
+
When your job is run, you will be able to access the values supplied on the
|
97
|
+
command line via the job's provided {BatchKit::Job#arguments #arguments}
|
98
|
+
accessor.
|
99
|
+
|
100
|
+
```
|
101
|
+
if arguments.verbose
|
102
|
+
# Do something verbosely
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
We now come to the meat of our job - the tasks it is going to perform when
|
107
|
+
run. Tasks are simply methods, but have added functionality wrapped around
|
108
|
+
them. To define a task, there are two approaches that can be used:
|
109
|
+
|
110
|
+
```
|
111
|
+
desc 'A one-line description for my task'
|
112
|
+
task :method1 do |param1|
|
113
|
+
# Define the steps this method is to perform
|
114
|
+
...
|
115
|
+
end
|
116
|
+
|
117
|
+
def method2(param1, param2)
|
118
|
+
# Another task method
|
119
|
+
...
|
120
|
+
end
|
121
|
+
task :method2, 'A short description for my task method'
|
122
|
+
```
|
123
|
+
|
124
|
+
Both methods are equivalent, and both leave your class with a method named
|
125
|
+
for the task (which is then invoked like any other method) - so use
|
126
|
+
whichever appraoch you prefer.
|
127
|
+
|
128
|
+
While performing actions in your job, you can make use of the
|
129
|
+
{BatchKit::Job#log #log} method to output logging at various levels:
|
130
|
+
|
131
|
+
```
|
132
|
+
log.config "Spec: #{arguments.spec}"
|
133
|
+
log.info "Doing some work now"
|
134
|
+
log.detail "Here are some more detailed messages about what we are doing"
|
135
|
+
log.warn "Oh-oh, looks like trouble ahead"
|
136
|
+
log.error "Oh no, an exception has occurred!"
|
137
|
+
```
|
138
|
+
|
139
|
+
Finally, we need a method that will act as the main entry point to the job. We
|
140
|
+
define a job method much like a task, but there should only be one job method
|
141
|
+
in our class:
|
142
|
+
|
143
|
+
```
|
144
|
+
job 'This job does XYZ' do
|
145
|
+
p1, p2, p3 = ...
|
146
|
+
method1(p1)
|
147
|
+
method2(p2, p3)
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
As with tasks, we can use the {BatchKit::ActsAsJob#job #job} DSL method above to
|
152
|
+
define the main entry method, or we can pass a symbol identifying an existing
|
153
|
+
method in our class to be the job entry point.
|
154
|
+
|
155
|
+
Finally, to allow our job to run when it is passed as the main program to the
|
156
|
+
Ruby engine, we call the {Batch::Job.run run} method on our class at the end of
|
157
|
+
our script:
|
158
|
+
|
159
|
+
```
|
160
|
+
MyJob.run
|
161
|
+
```
|
162
|
+
|
163
|
+
This instructs the batch kit framework to instantiate our job class, parse any
|
164
|
+
command-line arguments, and then invoke our job entry method to start processing.
|
165
|
+
|
data/lib/batch-kit.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# BatchKit is the namespace for the framework of functionality that includes:
|
2
|
+
# - BatchKit:Job
|
3
|
+
# - BatchKit::Config
|
4
|
+
# - BatchKit::Database
|
5
|
+
# - Core Ruby class extensions
|
6
|
+
# - Helper methods for common job requirements
|
7
|
+
class BatchKit; end
|
8
|
+
|
9
|
+
require 'batch-kit/job'
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'arg_parser'
|
2
|
+
require 'color-console'
|
3
|
+
|
4
|
+
|
5
|
+
class BatchKit
|
6
|
+
|
7
|
+
# Defines a module for adding argument parsing to a job via the arg-parser
|
8
|
+
# gem, and displaying help via the color-console gem.
|
9
|
+
module Arguments
|
10
|
+
|
11
|
+
include ArgParser::DSL
|
12
|
+
|
13
|
+
|
14
|
+
# Adds an arguments accessor that returns the results from parsing the
|
15
|
+
# command-line.
|
16
|
+
attr_reader :arguments
|
17
|
+
|
18
|
+
|
19
|
+
# Parse command-line arguments
|
20
|
+
def parse_arguments(args = ARGV, show_usage_on_error = true)
|
21
|
+
if defined?(BatchKit::Job) && self.is_a?(BatchKit::Job)
|
22
|
+
args_def.title ||= self.job.name.titleize
|
23
|
+
args_def.purpose ||= self.job.description
|
24
|
+
elsif defined?(BatchKit::Sequence) && self.is_a?(BatchKit::Sequence)
|
25
|
+
args_def.title ||= self.sequence.name.titleize
|
26
|
+
args_def.purpose ||= self.sequence.description
|
27
|
+
end
|
28
|
+
arg_parser = ArgParser::Parser.new(args_def)
|
29
|
+
@arguments = arg_parser.parse(args)
|
30
|
+
if @arguments == false
|
31
|
+
if arg_parser.show_help?
|
32
|
+
arg_parser.definition.show_help(nil, Console.width || 80).each do |line|
|
33
|
+
Console.puts line, :cyan
|
34
|
+
end
|
35
|
+
exit
|
36
|
+
else
|
37
|
+
arg_parser.errors.each{ |error| Console.puts error, :red }
|
38
|
+
if show_usage_on_error
|
39
|
+
arg_parser.definition.show_usage(nil, Console.width || 80).each do |line|
|
40
|
+
Console.puts line, :yellow
|
41
|
+
end
|
42
|
+
end
|
43
|
+
exit(99)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
@arguments
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# Add class methods when module is included
|
51
|
+
def self.included(base)
|
52
|
+
base.extend(ClassMethods)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,517 @@
|
|
1
|
+
class BatchKit
|
2
|
+
|
3
|
+
# Defines a class for managing configuration properties; essentially, this
|
4
|
+
# is a hash that is case and Strng/Symbol insensitive with respect to keys;
|
5
|
+
# this means a value can be retrieved using any mix of case using either a
|
6
|
+
# String or Symbol as the lookup key.
|
7
|
+
#
|
8
|
+
# In addition, there are some further conveniences added on:
|
9
|
+
# - Items can be accessed by either [] (using a String or Symbol key) or as
|
10
|
+
# methods on the Config object, i.e. the following are all equivalent:
|
11
|
+
#
|
12
|
+
# config['Foo']
|
13
|
+
# config[:foo]
|
14
|
+
# config.foo
|
15
|
+
#
|
16
|
+
# - Contents can be loaded from:
|
17
|
+
# - an existing Hash object
|
18
|
+
# - a properties file using [Section] and KEY=VALUE syntax
|
19
|
+
# - a YAML file
|
20
|
+
# - String values can contain placeholder variables that will be replaced
|
21
|
+
# when the item is added to the Config collection. Placeholder variables
|
22
|
+
# are denoted by ${<variable>} or %{<variable>} syntax, where <variable>
|
23
|
+
# is the name of another configuration variable or a key in a supplied
|
24
|
+
# expansion properties hash.
|
25
|
+
# - Support for encrypted values. These are decrypted on the fly, provided
|
26
|
+
# a decryption key has been set on the Config object.
|
27
|
+
#
|
28
|
+
# Internally, the case and String/Symbol insensitivity is managed by
|
29
|
+
# maintaining a second Hash that converts the lower-cased symbol value
|
30
|
+
# of all keys to the actual keys used to store the object. When looking up
|
31
|
+
# a value, we use this lookup Hash to find the actual key used, and then
|
32
|
+
# lookup the value.
|
33
|
+
#
|
34
|
+
# @note As this Config object is case and String/Symbol insensitive,
|
35
|
+
# different case and type keys that convert to the same lookup key are
|
36
|
+
# considered the same. The practical implication is that you can't have
|
37
|
+
# two different values in this Config object where the keys differ only
|
38
|
+
# in case and/or String/Symbol class.
|
39
|
+
class Config < Hash
|
40
|
+
|
41
|
+
# Create a new Config object, and initialize it from the specified
|
42
|
+
# +file+.
|
43
|
+
#
|
44
|
+
# @param file [String] A path to a properties or YAML file to load.
|
45
|
+
# @param props [Hash, Config] An optional Hash (or Config) object to
|
46
|
+
# seed this Config object with.
|
47
|
+
# @param options [Hash] An options hash.
|
48
|
+
# @option options [Boolean] :raise_on_unknown_var Whether to raise an
|
49
|
+
# error if an unrecognised placeholder variable is encountered in the
|
50
|
+
# file.
|
51
|
+
# @option options [Boolean] :use_erb If true, the contents of +file+
|
52
|
+
# is first run through ERB.
|
53
|
+
# @option options [Binding] :binding The binding to use when evaluating
|
54
|
+
# expressions in ERB
|
55
|
+
# @return [Config] A new Config object populated from +file+ and
|
56
|
+
# +props+, where placeholder variables have been expanded.
|
57
|
+
def self.load(file, props = nil, options = {})
|
58
|
+
cfg = self.new(props, options)
|
59
|
+
cfg.load(file, options)
|
60
|
+
cfg
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Converts a +str+ in the form of a properties file into a Hash.
|
65
|
+
def self.properties_to_hash(str)
|
66
|
+
hsh = props = {}
|
67
|
+
str.each_line do |line|
|
68
|
+
line.chomp!
|
69
|
+
if match = /^\s*\[([A-Za-z0-9_ ]+)\]\s*$/.match(line)
|
70
|
+
# Section heading
|
71
|
+
props = hsh[match[1]] = {}
|
72
|
+
elsif match = /^\s*([A-Za-z0-9_\.]+)\s*=\s*([^#]+)/.match(line)
|
73
|
+
# Property setting
|
74
|
+
val = match[2]
|
75
|
+
props[match[1]] = case val
|
76
|
+
when /^\d+$/ then val.to_i
|
77
|
+
when /^\d*\.\d+$/ then val.to_f
|
78
|
+
when /^:/ then val.intern
|
79
|
+
when /false/i then false
|
80
|
+
when /true/i then true
|
81
|
+
else val
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
hsh
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# Expand any ${<variable>} or %{<variable>} placeholders in +str+ from
|
90
|
+
# either the supplied +props+ hash or the system environment variables.
|
91
|
+
# The props hash is assumed to contain string or symbol keys matching the
|
92
|
+
# variable name between ${ and } (or %{ and }) delimiters.
|
93
|
+
# If no match is found in the supplied props hash or the environment, the
|
94
|
+
# default behaviour returns the string with the placeholder variable still in
|
95
|
+
# place, but this behaviour can be overridden to cause an exception to be
|
96
|
+
# raised if desired.
|
97
|
+
#
|
98
|
+
# @param str [String] A String to be expanded from 0 or more placeholder
|
99
|
+
# substitutions
|
100
|
+
# @param properties [Hash, Array<Hash>] A properties Hash or array of Hashes
|
101
|
+
# from which placeholder variable values can be looked up.
|
102
|
+
# @param raise_on_unknown_var [Boolean] Whether or not an exception should
|
103
|
+
# be raised if no property is found for a placeholder expression. If false,
|
104
|
+
# unrecognised placeholder variables are left in the returned string.
|
105
|
+
# @return [String] A new string with placeholder variables replaced by
|
106
|
+
# the values in +props+.
|
107
|
+
def self.expand_placeholders(str, properties, raise_on_unknown_var = false)
|
108
|
+
chain = properties.is_a?(Hash) ? [properties] : properties.reverse
|
109
|
+
str.gsub(/(?:[$%])\{([a-zA-Z0-9_]+)\}/) do
|
110
|
+
case
|
111
|
+
when src = chain.find{ |props| props.has_key?($1) ||
|
112
|
+
props.has_key?($1.intern) ||
|
113
|
+
props.has_key?($1.downcase.intern) }
|
114
|
+
src[$1] || src[$1.intern] || src[$1.downcase.intern]
|
115
|
+
when ENV[$1] then ENV[$1]
|
116
|
+
when raise_on_unknown_var
|
117
|
+
raise KeyError, "No value supplied for placeholder variable '#{$&}'"
|
118
|
+
else
|
119
|
+
$&
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# Create a Config object, optionally initialized from +hsh+.
|
126
|
+
#
|
127
|
+
# @param hsh [Hash] An optional Hash to seed this Config object with.
|
128
|
+
# @param options [Hash] An options hash.
|
129
|
+
def initialize(hsh = nil, options = {})
|
130
|
+
super(nil)
|
131
|
+
@lookup_keys = {}
|
132
|
+
@decryption_key = nil
|
133
|
+
merge!(hsh, options) if hsh
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
# Read a properties or YAML file at the path specified in +path+, and
|
138
|
+
# load the contents to this Config object.
|
139
|
+
#
|
140
|
+
# @param path [String] The path to the properties or YAML file to be
|
141
|
+
# loaded.
|
142
|
+
# @param options [Hash] An options hash.
|
143
|
+
# @option options [Boolean] @raise_on_unknown_var Whether to raise an
|
144
|
+
# error if an unrecognised placeholder variable is encountered in the
|
145
|
+
# file.
|
146
|
+
def load(path, options = {})
|
147
|
+
props = case File.extname(path)
|
148
|
+
when /\.yaml/i then self.load_yaml(path, options)
|
149
|
+
else self.load_properties(path, options)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Process a property file, returning its contents as a Hash.
|
155
|
+
# Only lines of the form KEY=VALUE are processed, and # indicates the start
|
156
|
+
# of a comment. Property files can contain sections, denoted by [SECTION].
|
157
|
+
#
|
158
|
+
# @example
|
159
|
+
# If a properties file contains the following:
|
160
|
+
#
|
161
|
+
# FOO=Bar # This is a comment
|
162
|
+
# BAR=${FOO}\Baz
|
163
|
+
#
|
164
|
+
# [BAT]
|
165
|
+
# Car=Ford
|
166
|
+
#
|
167
|
+
# Then we would return a Config object containing the following:
|
168
|
+
#
|
169
|
+
# {'FOO' => 'Bar', 'BAR' => 'Bar\Baz', 'BAT' => {'Car' => 'Ford'}}
|
170
|
+
#
|
171
|
+
# This config content could be accessed via #[] or as properties of
|
172
|
+
# the Config object, e.g.
|
173
|
+
#
|
174
|
+
# cfg[:foo] # => 'Bar'
|
175
|
+
# cfg.bar # => 'Bar\Baz'
|
176
|
+
# cfg.bat.car # => 'Ford'
|
177
|
+
#
|
178
|
+
# @param prop_file [String] A path to the properties file to be parsed.
|
179
|
+
# @param options [Hash] An options hash.
|
180
|
+
# @option options [Boolean] :use_erb If true, the contents of +prop_file+
|
181
|
+
# is first passed through ERB before being processed. This allows for
|
182
|
+
# the use of <% %> and <%= %> directives in +prop_file+. The binding
|
183
|
+
# passed to ERB is the value of any :binding option specified, or else
|
184
|
+
# this Config object. If not specified, ERB is used if the file is
|
185
|
+
# found to contain the string '<%'.
|
186
|
+
# @option options [Binding] :binding The binding for ERB to use when
|
187
|
+
# processing expressions. Defaults to this Config instance if not
|
188
|
+
# specified.
|
189
|
+
# @return [Hash] The parsed contents of the file as a Hash.
|
190
|
+
def load_properties(prop_file, options = {})
|
191
|
+
str = read_file(prop_file, options)
|
192
|
+
hsh = self.class.properties_to_hash(str)
|
193
|
+
self.merge!(hsh, options)
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
# Load the YAML file at +yaml_file+.
|
198
|
+
#
|
199
|
+
# @param yaml_file [String] A path to a YAML file to be loaded.
|
200
|
+
# @param options [Hash] An options hash.
|
201
|
+
# @option options [Boolean] :use_erb If true, the contents of +yaml_file+
|
202
|
+
# are run through ERB before being parsed as YAML. This allows for use
|
203
|
+
# of <% %> and <%= %> directives in +yaml_file+. The binding passed to
|
204
|
+
# ERB is the value of any :binding option specified, or else this
|
205
|
+
# Config object. If not specified, ERB is used if the file is found to
|
206
|
+
# contain the string '<%'.
|
207
|
+
# @option options [Binding] :binding The binding for ERB to use when
|
208
|
+
# processing expressions. Defaults to this Config instance if not
|
209
|
+
# specified.
|
210
|
+
# @return [Object] The results of parsing the YAML contents of +yaml_file+.
|
211
|
+
def load_yaml(yaml_file, options = {})
|
212
|
+
require 'yaml'
|
213
|
+
str = read_file(yaml_file, options)
|
214
|
+
yaml = YAML.load(str)
|
215
|
+
self.merge!(yaml, options)
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
# Save this config object as a YAML file at +yaml_file+.
|
220
|
+
#
|
221
|
+
# @param yaml_file [String] A path to the YAML file to be saved.
|
222
|
+
# @param options [Hash] An options hash.
|
223
|
+
def save_yaml(yaml_file, options = {})
|
224
|
+
require 'yaml'
|
225
|
+
str = self.to_yaml
|
226
|
+
File.open(yaml_file, 'wb'){ |f| f.puts(str) }
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
# Ensure that Config objects are saved as normal hashes when writing
|
231
|
+
# YAML.
|
232
|
+
def encode_with(coder)
|
233
|
+
coder.represent_map nil, self
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
# Merge the contents of the specified +hsh+ into this Config object.
|
238
|
+
#
|
239
|
+
# @param hsh [Hash] The Hash object to merge into this Config object.
|
240
|
+
# @param options [Hash] An options hash.
|
241
|
+
# @option options [Boolean] :raise_on_unknown_var Whether to raise an
|
242
|
+
# exception if an unrecognised placeholder variable is encountered in
|
243
|
+
# +hsh+.
|
244
|
+
def merge!(hsh, options = {})
|
245
|
+
if hsh && !hsh.is_a?(Hash)
|
246
|
+
raise ArgumentError, "Only Hash objects can be merged into Config (got #{hsh.class.name})"
|
247
|
+
end
|
248
|
+
hsh && hsh.each do |key, val|
|
249
|
+
self[key] = convert_val(val, options[:raise_on_unknown_var])
|
250
|
+
end
|
251
|
+
if hsh.is_a?(Config)
|
252
|
+
@decryption_key = hsh.instance_variable_get(:@decryption_key) unless @decryption_key
|
253
|
+
end
|
254
|
+
self
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
# Merge the contents of the specified +hsh+ into a new Config object.
|
259
|
+
#
|
260
|
+
# @param hsh [Hash] The Hash object to merge with this Config object.
|
261
|
+
# @param options [Hash] An options hash.
|
262
|
+
# @option options [Boolean] @raise_on_unknown_var Whether to raise an
|
263
|
+
# error if an unrecognised placeholder variable is encountered in the
|
264
|
+
# file.
|
265
|
+
# @return A new Config object with the combined contents of this Config
|
266
|
+
# object plus the contents of +hsh+.
|
267
|
+
def merge(hsh, options = {})
|
268
|
+
cfg = self.dup
|
269
|
+
cfg.merge!(hsh, options)
|
270
|
+
cfg
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
# If set, encrypted strings (only) will be decrypted when accessed via
|
275
|
+
# #[] or #method_missing (for property-like access, e.g. +cfg.password+).
|
276
|
+
#
|
277
|
+
# @param key [String] The master encryption key used to encrypt sensitive
|
278
|
+
# values in this Config object.
|
279
|
+
def decryption_key=(key)
|
280
|
+
require_relative 'encryption'
|
281
|
+
self.each do |_, val|
|
282
|
+
val.decryption_key = key if val.is_a?(Config)
|
283
|
+
end
|
284
|
+
@decryption_key = key
|
285
|
+
end
|
286
|
+
alias_method :encryption_key=, :decryption_key=
|
287
|
+
|
288
|
+
|
289
|
+
# Recursively encrypts the values of all keys in this Config object that
|
290
|
+
# match +key_pat+.
|
291
|
+
#
|
292
|
+
# Note: +key_pat+ will be compared against the standardised key values of
|
293
|
+
# each object (i.e. lowercase, with spaces converted to _).
|
294
|
+
#
|
295
|
+
# @param key_pat [Regexp|String] A regular expression to be used to identify
|
296
|
+
# the keys that should be encrypted, e.g. /password/ would encrypt all
|
297
|
+
# values that have "password" in their key.
|
298
|
+
# @param master_key [String] The master key that should be used when
|
299
|
+
# encrypting. If not specified, uses the current value of the
|
300
|
+
# +decryption_key+ set for this Config object.
|
301
|
+
def encrypt(key_pat, master_key = @decryption_key)
|
302
|
+
key_pat = Regexp.new(key_pat, true) if key_pat.is_a?(String)
|
303
|
+
raise ArgumentError, "key_pat must be a Regexp or String" unless key_pat.is_a?(Regexp)
|
304
|
+
raise ArgumentError, "No master key has been set or passed" unless master_key
|
305
|
+
require_relative 'encryption'
|
306
|
+
self.each do |key, val|
|
307
|
+
if Config === val
|
308
|
+
val.encrypt(key_pat, master_key)
|
309
|
+
else
|
310
|
+
if @decryption_key && val.is_a?(String) && val =~ /!AES:([a-zA-Z0-9\/+=]+)!/
|
311
|
+
# Decrypt using old master key
|
312
|
+
val = self[key]
|
313
|
+
self[key] = val
|
314
|
+
end
|
315
|
+
if val.is_a?(String) && convert_key(key) =~ key_pat
|
316
|
+
self[key] = "!AES:#{Encryption.encrypt(master_key, val).strip}!"
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
@decryption_key = master_key
|
321
|
+
end
|
322
|
+
|
323
|
+
|
324
|
+
# Override #[] to be agnostic as to the case of the key, and whether it
|
325
|
+
# is a String or a Symbol.
|
326
|
+
def [](key)
|
327
|
+
key = @lookup_keys[convert_key(key)]
|
328
|
+
val = super(key)
|
329
|
+
if @decryption_key && val.is_a?(String) && val =~ /!AES:([a-zA-Z0-9\/+=]+)!/
|
330
|
+
begin
|
331
|
+
val = Encryption.decrypt(@decryption_key, $1)
|
332
|
+
rescue Exception => ex
|
333
|
+
raise "An error occurred while decrypting the value for key '#{key}': #{ex.message}"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
val
|
337
|
+
end
|
338
|
+
|
339
|
+
|
340
|
+
# Override #[]= to be agnostic as to the case of the key, and whether it
|
341
|
+
# is a String or a Symbol.
|
342
|
+
def []=(key, val)
|
343
|
+
std_key = convert_key(key)
|
344
|
+
if @lookup_keys[std_key] != key
|
345
|
+
delete(key)
|
346
|
+
@lookup_keys[std_key] = key
|
347
|
+
end
|
348
|
+
super key, val
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
# Override #delete to be agnostic as to the case of the key, and whether
|
353
|
+
# it is a String or a Symbol.
|
354
|
+
def delete(key)
|
355
|
+
key = @lookup_keys.delete(convert_key(key))
|
356
|
+
super key
|
357
|
+
end
|
358
|
+
|
359
|
+
|
360
|
+
# Override #has_key? to be agnostic as to the case of the key, and whether
|
361
|
+
# it is a String or a Symbol.
|
362
|
+
def has_key?(key)
|
363
|
+
key = @lookup_keys[convert_key(key)]
|
364
|
+
super key
|
365
|
+
end
|
366
|
+
alias_method :include?, :has_key?
|
367
|
+
|
368
|
+
|
369
|
+
# Override #fetch to be agnostic as to the case of the key, and whether it
|
370
|
+
# is a String or a Symbol.
|
371
|
+
def fetch(key, *rest)
|
372
|
+
key = @lookup_keys[convert_key(key)] || key
|
373
|
+
super
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
# Override #clone to also clone contents of @lookup_keys.
|
378
|
+
def clone
|
379
|
+
copy = super
|
380
|
+
copy.instance_variable_set(:@lookup_keys, @lookup_keys.clone)
|
381
|
+
copy
|
382
|
+
end
|
383
|
+
|
384
|
+
|
385
|
+
# Override #dup to also clone contents of @lookup_keys.
|
386
|
+
def dup
|
387
|
+
copy = super
|
388
|
+
copy.instance_variable_set(:@lookup_keys, @lookup_keys.dup)
|
389
|
+
copy
|
390
|
+
end
|
391
|
+
|
392
|
+
|
393
|
+
# Override Hash core extension method #to_cfg and just return self.
|
394
|
+
def to_cfg
|
395
|
+
self
|
396
|
+
end
|
397
|
+
|
398
|
+
|
399
|
+
# Override method_missing to respond to method calls with the value of the
|
400
|
+
# property, if this Config object contains a property of the same name.
|
401
|
+
def method_missing(name, *args)
|
402
|
+
if name =~ /^(.+)\?$/
|
403
|
+
has_key?($1)
|
404
|
+
elsif has_key?(name)
|
405
|
+
self[name]
|
406
|
+
elsif has_key?(name.to_s.gsub('_', ''))
|
407
|
+
self[name.to_s.gsub('_', '')]
|
408
|
+
elsif name =~ /^(.+)=$/
|
409
|
+
self[$1]= args.first
|
410
|
+
else
|
411
|
+
raise ArgumentError, "No configuration entry for key '#{name}'"
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
|
416
|
+
# Override respond_to? to indicate which methods we will accept.
|
417
|
+
def respond_to?(name)
|
418
|
+
if name =~ /^(.+)\?$/
|
419
|
+
has_key?($1)
|
420
|
+
elsif has_key?(name)
|
421
|
+
true
|
422
|
+
elsif has_key?(name.to_s.gsub('_', ''))
|
423
|
+
true
|
424
|
+
elsif name =~ /^(.+)=$/
|
425
|
+
true
|
426
|
+
else
|
427
|
+
super
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
|
432
|
+
# Expand any ${<variable>} or %{<variable>} placeholders in +str+ from
|
433
|
+
# this Config object or the system environment variables.
|
434
|
+
# This Config object is assumed to contain string or symbol keys matching
|
435
|
+
# the variable name between ${ and } (or %{ and }) delimiters.
|
436
|
+
# If no match is found in the supplied props hash or the environment, the
|
437
|
+
# default behaviour is to raise an exception, but this can be overriden
|
438
|
+
# to leave the placeholder variable still in place if desired.
|
439
|
+
#
|
440
|
+
# @param str [String] A String to be expanded from 0 or more placeholder
|
441
|
+
# substitutions
|
442
|
+
# @param raise_on_unknown_var [Boolean] Whether or not an exception should
|
443
|
+
# be raised if no property is found for a placeholder expression. If false,
|
444
|
+
# unrecognised placeholder variables are left in the returned string.
|
445
|
+
# @return [String] A new string with placeholder variables replaced by
|
446
|
+
# the values in +props+.
|
447
|
+
def expand_placeholders(str, raise_on_unknown_var = true)
|
448
|
+
self.class.expand_placeholders(str, self, raise_on_unknown_var)
|
449
|
+
end
|
450
|
+
|
451
|
+
|
452
|
+
# Reads a template file at +template_name+, and expands any substitution
|
453
|
+
# variable placeholder strings from this Config object.
|
454
|
+
#
|
455
|
+
# @param template_name [String] The path to the template file containing
|
456
|
+
# placeholder variables to expand from this Config object.
|
457
|
+
# @return [String] The contents of the template file with placeholder
|
458
|
+
# variables replaced by the content of this Config object.
|
459
|
+
def read_template(template_name, raise_on_unknown_var = true)
|
460
|
+
template = IO.read(template_name)
|
461
|
+
expand_placeholders(template, raise_on_unknown_var)
|
462
|
+
end
|
463
|
+
|
464
|
+
|
465
|
+
private
|
466
|
+
|
467
|
+
|
468
|
+
# Reads the contents of +file+ into a String. The +file+ is passed
|
469
|
+
# through ERB if the :use_erb option is true, or if the options does
|
470
|
+
# not contain the :use_erb key and the file does contain the string
|
471
|
+
# '<%'.
|
472
|
+
def read_file(file, options)
|
473
|
+
str = IO.read(file)
|
474
|
+
if (options.has_key?(:use_erb) && options[:use_erb]) || str =~ /<%/
|
475
|
+
require 'erb'
|
476
|
+
str = ERB.new(str).result(options[:binding] || binding)
|
477
|
+
end
|
478
|
+
str
|
479
|
+
end
|
480
|
+
|
481
|
+
|
482
|
+
# Convert the supplied key to a lower-case symbol representation, which
|
483
|
+
# is the key to the @lookup_keys hash.
|
484
|
+
def convert_key(key)
|
485
|
+
key.to_s.downcase.gsub(' ', '_').intern
|
486
|
+
end
|
487
|
+
|
488
|
+
|
489
|
+
# Convert a value before merging it into the Config. This consists of
|
490
|
+
# these tasks:
|
491
|
+
# - Converting Hashes to Config objects
|
492
|
+
# - Propogating decryption keys to child Config objects
|
493
|
+
# - Expanding placeholder variables in strings
|
494
|
+
def convert_val(val, raise_on_unknown_var, parents = [self])
|
495
|
+
case val
|
496
|
+
when Config then val
|
497
|
+
when Hash
|
498
|
+
cfg = Config.new
|
499
|
+
cfg.instance_variable_set(:@decryption_key, @decryption_key)
|
500
|
+
new_parents = parents.clone
|
501
|
+
new_parents << cfg
|
502
|
+
val.each do |k, v|
|
503
|
+
cfg[k] = convert_val(v, raise_on_unknown_var, new_parents)
|
504
|
+
end
|
505
|
+
cfg
|
506
|
+
when Array
|
507
|
+
val.map{ |v| convert_val(v, raise_on_unknown_var, parents) }
|
508
|
+
when /[$%]\{[a-zA-Z0-9_]+\}/
|
509
|
+
self.class.expand_placeholders(val, parents, raise_on_unknown_var)
|
510
|
+
else val
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
end
|
515
|
+
|
516
|
+
end
|
517
|
+
|