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
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
|
+
|