athena 0.1.5 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +1 -1
- data/Rakefile +1 -1
- data/bin/athena +2 -155
- data/lib/athena.rb +4 -7
- data/lib/athena/cli.rb +166 -0
- data/lib/athena/formats.rb +81 -75
- data/lib/athena/formats/dbm.rb +2 -3
- data/lib/athena/formats/ferret.rb +4 -4
- data/lib/athena/formats/lingo.rb +2 -3
- data/lib/athena/formats/sisis.rb +4 -4
- data/lib/athena/formats/sql.rb +8 -7
- data/lib/athena/formats/xml.rb +12 -71
- data/lib/athena/parser.rb +52 -52
- data/lib/athena/record.rb +44 -50
- data/lib/athena/version.rb +2 -2
- metadata +13 -13
- data/lib/athena/util.rb +0 -49
data/README
CHANGED
data/Rakefile
CHANGED
@@ -14,7 +14,7 @@ begin
|
|
14
14
|
:summary => %q{Convert database files to various formats.},
|
15
15
|
:author => %q{Jens Wille},
|
16
16
|
:email => %q{jens.wille@uni-koeln.de},
|
17
|
-
:dependencies => [
|
17
|
+
:dependencies => %w[builder xmlstreamin] << ['ruby-nuggets', '>= 0.7.4']
|
18
18
|
}
|
19
19
|
}}
|
20
20
|
rescue LoadError => err
|
data/bin/athena
CHANGED
@@ -28,158 +28,5 @@
|
|
28
28
|
###############################################################################
|
29
29
|
#++
|
30
30
|
|
31
|
-
require '
|
32
|
-
|
33
|
-
|
34
|
-
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
35
|
-
|
36
|
-
require 'athena'
|
37
|
-
|
38
|
-
USAGE = "Usage: #{$0} [-h|--help] [options]"
|
39
|
-
abort USAGE if ARGV.empty?
|
40
|
-
|
41
|
-
# Global variable to handle verbosity
|
42
|
-
$Verbose = {}
|
43
|
-
|
44
|
-
options = {
|
45
|
-
:config => 'config.yaml',
|
46
|
-
:input => STDIN,
|
47
|
-
:output => STDOUT,
|
48
|
-
:target => nil
|
49
|
-
}
|
50
|
-
|
51
|
-
OptionParser.new { |opts|
|
52
|
-
opts.banner = USAGE
|
53
|
-
|
54
|
-
opts.separator ''
|
55
|
-
opts.separator 'Options:'
|
56
|
-
|
57
|
-
opts.on('-c', '--config YAML', "Config file [Default: #{options[:config]}#{' (currently not present)' unless File.readable?(options[:config])}]") { |f|
|
58
|
-
abort "Can't find config file: #{f}." unless File.readable?(f)
|
59
|
-
|
60
|
-
options[:config] = f
|
61
|
-
}
|
62
|
-
|
63
|
-
opts.separator ''
|
64
|
-
|
65
|
-
opts.on('-i', '--input FILE', "Input file [Default: STDIN]") { |f|
|
66
|
-
abort "Can't find input file: #{f}." unless File.readable?(f)
|
67
|
-
|
68
|
-
options[:input] = File.directory?(f) ? Dir.open(f) : File.open(f, 'r')
|
69
|
-
|
70
|
-
p = File.basename(f).split('.')
|
71
|
-
options[:spec_fallback] = p.last.downcase
|
72
|
-
options[:target_fallback] = p.size > 1 ? p[0..-2].join('.') : p.first
|
73
|
-
}
|
74
|
-
|
75
|
-
opts.on('-s', '--spec SPEC', "Input format (spec) [Default: file extension of <input-file>]") { |s|
|
76
|
-
options[:spec] = s.downcase
|
77
|
-
}
|
78
|
-
|
79
|
-
opts.on('-L', '--list-specs', "List available input formats (specs) and exit") {
|
80
|
-
puts 'Available input formats (specs):'
|
81
|
-
|
82
|
-
Athena.input_formats.each { |f, k|
|
83
|
-
puts " - #{f}#{" (= #{k})" if f != k.to_s}"
|
84
|
-
}
|
85
|
-
|
86
|
-
exit 0
|
87
|
-
}
|
88
|
-
|
89
|
-
opts.separator ''
|
90
|
-
|
91
|
-
opts.on('-o', '--output FILE', "Output file [Default: STDOUT]") { |f|
|
92
|
-
options[:output] = File.open(f, 'w')
|
93
|
-
|
94
|
-
options[:format_fallback] = f.split('.').last.downcase
|
95
|
-
}
|
96
|
-
|
97
|
-
opts.on('-f', '--format FORMAT', "Output format [Default: file extension of <output-file>]") { |f|
|
98
|
-
options[:format] = f.downcase
|
99
|
-
}
|
100
|
-
|
101
|
-
opts.on('-l', '--list-formats', "List available output formats and exit") {
|
102
|
-
puts 'Available output formats:'
|
103
|
-
|
104
|
-
Athena.output_formats.each { |f, k|
|
105
|
-
puts " - #{f}#{" (= #{k})" if f != k.to_s}"
|
106
|
-
}
|
107
|
-
|
108
|
-
exit 0
|
109
|
-
}
|
110
|
-
|
111
|
-
opts.separator ''
|
112
|
-
|
113
|
-
opts.on('-t', '--target ID', "Target whose config to use [Default: <input-file> minus file extension,", "plus '.<spec>', plus ':<format>' (reversely in turn)]") { |t|
|
114
|
-
options[:target] = t
|
115
|
-
}
|
116
|
-
|
117
|
-
opts.separator ''
|
118
|
-
opts.separator 'Generic options:'
|
119
|
-
|
120
|
-
opts.on('-v', '--verbose [WHAT]', "Be verbose about what's being done. Optional argument is a comma-separated", "list of what should be output, or 'all' [Default: 'all']") { |what|
|
121
|
-
if what.nil? || what == 'all'
|
122
|
-
$Verbose.default = true
|
123
|
-
else
|
124
|
-
what.split(',').each { |w|
|
125
|
-
$Verbose[w.to_sym] = true
|
126
|
-
}
|
127
|
-
end
|
128
|
-
}
|
129
|
-
|
130
|
-
opts.on('-h', '--help', 'Print this help message and exit') {
|
131
|
-
abort opts.to_s
|
132
|
-
}
|
133
|
-
|
134
|
-
opts.on('--version', 'Print program version and exit') {
|
135
|
-
abort "#{File.basename($0)} v#{Athena::VERSION}"
|
136
|
-
}
|
137
|
-
}.parse!
|
138
|
-
|
139
|
-
spec = options[:spec] || options[:spec_fallback]
|
140
|
-
abort "No input format (spec) specified and none could be inferred." unless spec
|
141
|
-
abort "Invalid input format (spec): #{spec}. Use '-L' to get a list of available specs." unless Athena.valid_input_format?(spec)
|
142
|
-
|
143
|
-
format = options[:format] || options[:format_fallback]
|
144
|
-
abort "No output format specified and none could be inferred." unless format
|
145
|
-
abort "Invalid output format: #{format}. Use '-l' to get a list of available formats." unless Athena.valid_output_format?(format)
|
146
|
-
|
147
|
-
yaml = YAML.load_file(options[:config])
|
148
|
-
if t = options[:target]
|
149
|
-
target = t
|
150
|
-
config = yaml[t.to_sym]
|
151
|
-
else
|
152
|
-
[options[:target_fallback] || 'generic', ".#{spec}", ":#{format}"].inject([]) { |s, t|
|
153
|
-
s << (s.last ? s.last + t : t)
|
154
|
-
}.reverse.find { |t|
|
155
|
-
target = t
|
156
|
-
config = yaml[t.to_sym]
|
157
|
-
}
|
158
|
-
end
|
159
|
-
abort "Config not found for target: #{target}." unless config
|
160
|
-
|
161
|
-
parser = Athena.parser(config, spec)
|
162
|
-
|
163
|
-
if Athena.deferred_output?(format)
|
164
|
-
res = parser.parse(options[:input])
|
165
|
-
|
166
|
-
res.map { |record|
|
167
|
-
record.to(format)
|
168
|
-
}.flatten.sort.uniq.each { |line|
|
169
|
-
options[:output].puts line
|
170
|
-
}
|
171
|
-
elsif Athena.raw_output?(format)
|
172
|
-
res = Athena.with_format(format, options[:output]) { |_format|
|
173
|
-
parser.parse(options[:input]) { |record|
|
174
|
-
record.to(_format)
|
175
|
-
}
|
176
|
-
}
|
177
|
-
else
|
178
|
-
res = Athena.with_format(format) { |_format|
|
179
|
-
parser.parse(options[:input]) { |record|
|
180
|
-
options[:output].puts record.to(_format)
|
181
|
-
}
|
182
|
-
}
|
183
|
-
end
|
184
|
-
|
185
|
-
Athena::Util.verbose(:count) { spit res.is_a?(Numeric) ? res : res.size }
|
31
|
+
require 'athena/cli'
|
32
|
+
Athena::CLI.execute
|
data/lib/athena.rb
CHANGED
@@ -35,17 +35,14 @@
|
|
35
35
|
# instance method _convert_ supplied. This way, a specific format can even function
|
36
36
|
# as both input and output format.
|
37
37
|
|
38
|
-
module Athena
|
39
|
-
end
|
40
|
-
|
41
|
-
require 'athena/util'
|
42
|
-
require 'athena/parser'
|
43
|
-
require 'athena/record'
|
44
|
-
require 'athena/formats'
|
45
38
|
require 'athena/version'
|
46
39
|
|
47
40
|
module Athena
|
48
41
|
|
42
|
+
autoload :Parser, 'athena/parser'
|
43
|
+
autoload :Record, 'athena/record'
|
44
|
+
autoload :Formats, 'athena/formats'
|
45
|
+
|
49
46
|
extend self
|
50
47
|
|
51
48
|
def parser(config, format)
|
data/lib/athena/cli.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
#--
|
2
|
+
###############################################################################
|
3
|
+
# #
|
4
|
+
# A component of athena, the database file converter. #
|
5
|
+
# #
|
6
|
+
# Copyright (C) 2007-2011 University of Cologne, #
|
7
|
+
# Albertus-Magnus-Platz, #
|
8
|
+
# 50923 Cologne, Germany #
|
9
|
+
# #
|
10
|
+
# Authors: #
|
11
|
+
# Jens Wille <jens.wille@uni-koeln.de> #
|
12
|
+
# #
|
13
|
+
# athena is free software; you can redistribute it and/or modify it under the #
|
14
|
+
# terms of the GNU Affero General Public License as published by the Free #
|
15
|
+
# Software Foundation; either version 3 of the License, or (at your option) #
|
16
|
+
# any later version. #
|
17
|
+
# #
|
18
|
+
# athena is distributed in the hope that it will be useful, but WITHOUT ANY #
|
19
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
20
|
+
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
|
21
|
+
# more details. #
|
22
|
+
# #
|
23
|
+
# You should have received a copy of the GNU Affero General Public License #
|
24
|
+
# along with athena. If not, see <http://www.gnu.org/licenses/>. #
|
25
|
+
# #
|
26
|
+
###############################################################################
|
27
|
+
#++
|
28
|
+
|
29
|
+
require 'nuggets/util/cli'
|
30
|
+
require 'athena'
|
31
|
+
|
32
|
+
module Athena
|
33
|
+
|
34
|
+
class CLI < ::Util::CLI
|
35
|
+
|
36
|
+
class << self
|
37
|
+
|
38
|
+
def defaults
|
39
|
+
super.merge(
|
40
|
+
:config => 'config.yaml',
|
41
|
+
:input => '-',
|
42
|
+
:output => '-',
|
43
|
+
:target => nil
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def run(arguments)
|
50
|
+
spec = options[:spec] || options[:spec_fallback]
|
51
|
+
abort "No input format (spec) specified and none could be inferred." unless spec
|
52
|
+
abort "Invalid input format (spec): #{spec}. Use `-L' to get a list of available specs." unless Athena.valid_input_format?(spec)
|
53
|
+
|
54
|
+
format = options[:format] || options[:format_fallback]
|
55
|
+
abort "No output format specified and none could be inferred." unless format
|
56
|
+
abort "Invalid output format: #{format}. Use `-l' to get a list of available formats." unless Athena.valid_output_format?(format)
|
57
|
+
|
58
|
+
target_config = if t = options[:target]
|
59
|
+
config[target = t.to_sym]
|
60
|
+
else
|
61
|
+
[options[:target_fallback] || 'generic', ".#{spec}", ":#{format}"].inject([]) { |s, t|
|
62
|
+
s << (s.last ? s.last + t : t)
|
63
|
+
}.reverse.find { |t| config[target = t.to_sym] }
|
64
|
+
end or abort "Config not found for target: #{target}."
|
65
|
+
|
66
|
+
parser = Athena.parser(target_config, spec)
|
67
|
+
|
68
|
+
input = options[:input]
|
69
|
+
input = arguments.shift unless input != defaults[:input] || arguments.empty?
|
70
|
+
options[:input] = File.directory?(input) ? Dir.open(input) : open_file_or_std(input)
|
71
|
+
|
72
|
+
quit unless arguments.empty?
|
73
|
+
|
74
|
+
options[:output] = open_file_or_std(options[:output], true)
|
75
|
+
|
76
|
+
if Athena.deferred_output?(format)
|
77
|
+
res = parser.parse(options[:input])
|
78
|
+
|
79
|
+
res.map { |record|
|
80
|
+
record.to(format)
|
81
|
+
}.flatten.sort.uniq.each { |line|
|
82
|
+
options[:output].puts line
|
83
|
+
}
|
84
|
+
elsif Athena.raw_output?(format)
|
85
|
+
res = Athena.with_format(format, options[:output]) { |_format|
|
86
|
+
parser.parse(options[:input]) { |record|
|
87
|
+
record.to(_format)
|
88
|
+
}
|
89
|
+
}
|
90
|
+
else
|
91
|
+
res = Athena.with_format(format) { |_format|
|
92
|
+
parser.parse(options[:input]) { |record|
|
93
|
+
options[:output].puts record.to(_format)
|
94
|
+
}
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def merge_config(args = [default])
|
102
|
+
super
|
103
|
+
end
|
104
|
+
|
105
|
+
def opts(opts)
|
106
|
+
opts.on('-c', '--config YAML', "Config file [Default: #{defaults[:config]}#{' (currently not present)' unless File.readable?(defaults[:config])}]") { |config|
|
107
|
+
quit "Can't find config file: #{config}" unless File.readable?(config)
|
108
|
+
|
109
|
+
options[:config] = config
|
110
|
+
}
|
111
|
+
|
112
|
+
opts.separator ''
|
113
|
+
|
114
|
+
opts.on('-i', '--input FILE', "Input file [Default: STDIN]") { |input|
|
115
|
+
options[:input] = input
|
116
|
+
|
117
|
+
parts = File.basename(input).split('.')
|
118
|
+
options[:spec_fallback] = parts.last.downcase
|
119
|
+
options[:target_fallback] = parts.size > 1 ? parts[0..-2].join('.') : parts.first
|
120
|
+
}
|
121
|
+
|
122
|
+
opts.on('-s', '--spec SPEC', "Input format (spec) [Default: file extension of <input-file>]") { |spec|
|
123
|
+
options[:spec] = spec.downcase
|
124
|
+
}
|
125
|
+
|
126
|
+
opts.on('-L', '--list-specs', "List available input formats (specs) and exit") {
|
127
|
+
puts 'Available input formats (specs):'
|
128
|
+
|
129
|
+
Athena.input_formats.each { |format, name|
|
130
|
+
puts " - #{format}#{" (= #{name})" if format != name.to_s}"
|
131
|
+
}
|
132
|
+
|
133
|
+
exit
|
134
|
+
}
|
135
|
+
|
136
|
+
opts.separator ''
|
137
|
+
|
138
|
+
opts.on('-o', '--output FILE', "Output file [Default: STDOUT]") { |output|
|
139
|
+
options[:output] = output
|
140
|
+
options[:format_fallback] = output.split('.').last.downcase
|
141
|
+
}
|
142
|
+
|
143
|
+
opts.on('-f', '--format FORMAT', "Output format [Default: file extension of <output-file>]") { |format|
|
144
|
+
options[:format] = format.downcase
|
145
|
+
}
|
146
|
+
|
147
|
+
opts.on('-l', '--list-formats', "List available output formats and exit") {
|
148
|
+
puts 'Available output formats:'
|
149
|
+
|
150
|
+
Athena.output_formats.each { |format, name|
|
151
|
+
puts " - #{format}#{" (= #{name})" if format != name.to_s}"
|
152
|
+
}
|
153
|
+
|
154
|
+
exit
|
155
|
+
}
|
156
|
+
|
157
|
+
opts.separator ''
|
158
|
+
|
159
|
+
opts.on('-t', '--target ID', "Target whose config to use [Default: <input-file> minus file extension,", "plus '.<spec>', plus ':<format>' (reversely in turn)]") { |target|
|
160
|
+
options[:target] = target
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
data/lib/athena/formats.rb
CHANGED
@@ -26,114 +26,120 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
+
require 'athena'
|
30
|
+
|
29
31
|
module Athena
|
32
|
+
|
30
33
|
module Formats
|
31
34
|
|
32
|
-
|
33
|
-
|
35
|
+
CRLF = %Q{\015\012}
|
36
|
+
CRLF_RE = %r{(?:\r?\n)+}
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
def self.[](direction, format)
|
39
|
+
if direction == :out
|
40
|
+
if format.class < Base
|
41
|
+
if format.class.direction != direction
|
42
|
+
raise DirectionMismatchError,
|
43
|
+
"expected #{direction}, got #{format.class.direction}"
|
44
|
+
else
|
45
|
+
format
|
46
|
+
end
|
41
47
|
else
|
42
|
-
format
|
48
|
+
Base.formats[direction][format].new
|
43
49
|
end
|
44
50
|
else
|
45
|
-
Base.formats[direction][format]
|
51
|
+
Base.formats[direction][format]
|
46
52
|
end
|
47
|
-
else
|
48
|
-
Base.formats[direction][format]
|
49
53
|
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class Base
|
53
54
|
|
54
|
-
|
55
|
+
class Base
|
55
56
|
|
56
|
-
|
57
|
+
@formats = { :in => {}, :out => {} }
|
57
58
|
|
58
|
-
|
59
|
-
Base.instance_variable_get(:@formats)
|
60
|
-
end
|
59
|
+
class << self
|
61
60
|
|
62
|
-
|
63
|
-
|
64
|
-
direction == format.class.direction
|
65
|
-
else
|
66
|
-
formats[direction].has_key?(format)
|
61
|
+
def formats
|
62
|
+
Base.instance_variable_get(:@formats)
|
67
63
|
end
|
68
|
-
end
|
69
64
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
65
|
+
def valid_format?(direction, format)
|
66
|
+
if format.class < Base
|
67
|
+
direction == format.class.direction
|
68
|
+
else
|
69
|
+
formats[direction].has_key?(format)
|
70
|
+
end
|
71
|
+
end
|
77
72
|
|
78
|
-
|
79
|
-
end
|
73
|
+
private
|
80
74
|
|
81
|
-
|
82
|
-
|
75
|
+
def register_format(direction, *aliases, &block)
|
76
|
+
format = name.split('::').last.
|
77
|
+
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
|
78
|
+
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
79
|
+
downcase
|
83
80
|
|
84
|
-
|
81
|
+
register_format!(direction, format, *aliases, &block)
|
82
|
+
end
|
85
83
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
84
|
+
def register_format!(direction, format, *aliases, &block)
|
85
|
+
raise "must be a sub-class of #{Base}" unless self < Base
|
86
|
+
|
87
|
+
klass = Class.new(self, &block)
|
88
|
+
|
89
|
+
klass.instance_eval %Q{
|
90
|
+
def direction; #{direction.inspect}; end
|
91
|
+
def name; '#{format}::#{direction}'; end
|
92
|
+
def to_s; '#{format}'; end
|
93
|
+
}
|
94
|
+
|
95
|
+
[format, *aliases].each { |name|
|
96
|
+
if existing = formats[direction][name]
|
97
|
+
raise DuplicateFormatDefinitionError,
|
98
|
+
"format already defined (#{direction}): #{name}"
|
99
|
+
else
|
100
|
+
formats[direction][name] = klass
|
101
|
+
end
|
102
|
+
}
|
103
|
+
end
|
91
104
|
|
92
|
-
[format, *aliases].each { |name|
|
93
|
-
if existing = formats[direction][name]
|
94
|
-
raise DuplicateFormatDefinitionError,
|
95
|
-
"format already defined (#{direction}): #{name}"
|
96
|
-
else
|
97
|
-
formats[direction][name] = klass
|
98
|
-
end
|
99
|
-
}
|
100
105
|
end
|
101
106
|
|
102
|
-
|
107
|
+
def parse(*args)
|
108
|
+
raise NotImplementedError, 'must be defined by sub-class'
|
109
|
+
end
|
103
110
|
|
104
|
-
|
105
|
-
|
106
|
-
|
111
|
+
def convert(record)
|
112
|
+
raise NotImplementedError, 'must be defined by sub-class'
|
113
|
+
end
|
107
114
|
|
108
|
-
|
109
|
-
|
110
|
-
|
115
|
+
def wrap
|
116
|
+
yield self
|
117
|
+
end
|
111
118
|
|
112
|
-
|
113
|
-
|
114
|
-
|
119
|
+
def deferred?
|
120
|
+
false
|
121
|
+
end
|
115
122
|
|
116
|
-
|
117
|
-
|
118
|
-
|
123
|
+
def raw?
|
124
|
+
false
|
125
|
+
end
|
119
126
|
|
120
|
-
def raw?
|
121
|
-
false
|
122
127
|
end
|
123
128
|
|
124
|
-
|
125
|
-
|
126
|
-
class FormatError < StandardError; end
|
129
|
+
class FormatError < StandardError; end
|
127
130
|
|
128
|
-
|
129
|
-
|
131
|
+
class DuplicateFormatDefinitionError < FormatError; end
|
132
|
+
class DirectionMismatchError < FormatError; end
|
130
133
|
|
131
|
-
|
134
|
+
ConfigError = Parser::ConfigError
|
132
135
|
|
133
|
-
|
134
|
-
|
136
|
+
class NoRecordElementError < ConfigError; end
|
137
|
+
class IllegalRecordElementError < ConfigError; end
|
135
138
|
|
136
139
|
end
|
140
|
+
|
137
141
|
end
|
138
142
|
|
139
|
-
Dir[__FILE__.sub(/\.rb\z/, '/**/*.rb')].sort.each { |rb|
|
143
|
+
Dir[__FILE__.sub(/\.rb\z/, '/**/*.rb')].sort.each { |rb|
|
144
|
+
require "athena/formats/#{File.basename(rb, '.rb')}"
|
145
|
+
}
|
data/lib/athena/formats/dbm.rb
CHANGED
@@ -26,6 +26,8 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
+
require 'athena'
|
30
|
+
|
29
31
|
if ferret_version = ENV['FERRET_VERSION']
|
30
32
|
require 'rubygems'
|
31
33
|
gem 'ferret', ferret_version
|
@@ -37,8 +39,7 @@ rescue LoadError => err
|
|
37
39
|
warn "ferret#{" #{ferret_version}" if ferret_version} not available (#{err})"
|
38
40
|
end
|
39
41
|
|
40
|
-
module Athena
|
41
|
-
module Formats
|
42
|
+
module Athena::Formats
|
42
43
|
|
43
44
|
class Ferret < Base
|
44
45
|
|
@@ -88,7 +89,7 @@ module Athena
|
|
88
89
|
unless index.deleted?(i)
|
89
90
|
doc = index[i]
|
90
91
|
|
91
|
-
Record.new(doc[record_element], block) { |record|
|
92
|
+
Athena::Record.new(doc[record_element], block) { |record|
|
92
93
|
config.each { |element, field_config|
|
93
94
|
record.update(element, doc[element], field_config)
|
94
95
|
}
|
@@ -101,5 +102,4 @@ module Athena
|
|
101
102
|
|
102
103
|
end
|
103
104
|
|
104
|
-
end
|
105
105
|
end
|
data/lib/athena/formats/lingo.rb
CHANGED
data/lib/athena/formats/sisis.rb
CHANGED
@@ -26,8 +26,9 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
require 'athena'
|
30
|
+
|
31
|
+
module Athena::Formats
|
31
32
|
|
32
33
|
class Sisis < Base
|
33
34
|
|
@@ -60,7 +61,7 @@ module Athena
|
|
60
61
|
|
61
62
|
if element == record_element
|
62
63
|
record.close if record
|
63
|
-
record = Record.new(value, block)
|
64
|
+
record = Athena::Record.new(value, block)
|
64
65
|
num += 1
|
65
66
|
else
|
66
67
|
record.update(element, value, config[element])
|
@@ -74,5 +75,4 @@ module Athena
|
|
74
75
|
|
75
76
|
end
|
76
77
|
|
77
|
-
end
|
78
78
|
end
|
data/lib/athena/formats/sql.rb
CHANGED
@@ -27,9 +27,9 @@
|
|
27
27
|
#++
|
28
28
|
|
29
29
|
require 'strscan'
|
30
|
+
require 'athena'
|
30
31
|
|
31
|
-
module Athena
|
32
|
-
module Formats
|
32
|
+
module Athena::Formats
|
33
33
|
|
34
34
|
class MYSQL < Base
|
35
35
|
|
@@ -68,7 +68,7 @@ module Athena
|
|
68
68
|
next if _columns.empty?
|
69
69
|
|
70
70
|
sql_parser.parse($2) { |row|
|
71
|
-
Record.new(nil, block) { |record|
|
71
|
+
Athena::Record.new(nil, block) { |record|
|
72
72
|
row.each_with_index { |value, index|
|
73
73
|
column = _columns[index] or next
|
74
74
|
|
@@ -158,7 +158,9 @@ module Athena
|
|
158
158
|
end
|
159
159
|
|
160
160
|
def parse_string_escape
|
161
|
-
if @input.scan(/\\[
|
161
|
+
if @input.scan(/\\[abtnvfr]/)
|
162
|
+
AST.new(eval(%Q{"#{@input.matched}"}))
|
163
|
+
elsif @input.scan(/\\.|''/)
|
162
164
|
AST.new(@input.matched[-1, 1])
|
163
165
|
end
|
164
166
|
end
|
@@ -179,7 +181,7 @@ module Athena
|
|
179
181
|
if @input.eos?
|
180
182
|
raise "Unexpected end of input (#{message})."
|
181
183
|
else
|
182
|
-
raise "#{message} at #{@input.pos}: #{@input.peek(
|
184
|
+
raise "#{message} at #{$.}:#{@input.pos}: #{@input.peek(16).inspect}"
|
183
185
|
end
|
184
186
|
end
|
185
187
|
|
@@ -221,7 +223,7 @@ module Athena
|
|
221
223
|
cols = columns[table]
|
222
224
|
next if cols.empty?
|
223
225
|
|
224
|
-
Record.new(nil, block) { |record|
|
226
|
+
Athena::Record.new(nil, block) { |record|
|
225
227
|
line.split(/\t/).each_with_index { |value, index|
|
226
228
|
column = cols[index] or next
|
227
229
|
|
@@ -245,5 +247,4 @@ module Athena
|
|
245
247
|
MySQL = MYSQL
|
246
248
|
PgSQL = PGSQL
|
247
249
|
|
248
|
-
end
|
249
250
|
end
|
data/lib/athena/formats/xml.rb
CHANGED
@@ -27,18 +27,15 @@
|
|
27
27
|
#++
|
28
28
|
|
29
29
|
require 'forwardable'
|
30
|
-
|
31
30
|
require 'builder'
|
32
31
|
require 'xmlstreamin'
|
33
32
|
require 'nuggets/hash/insert'
|
33
|
+
require 'athena'
|
34
34
|
|
35
|
-
module Athena
|
36
|
-
module Formats
|
35
|
+
module Athena::Formats
|
37
36
|
|
38
37
|
class XML < Base
|
39
38
|
|
40
|
-
include Util
|
41
|
-
|
42
39
|
# <http://www.w3.org/TR/2006/REC-xml-20060816/#NT-Name>
|
43
40
|
ELEMENT_START_RE = %r{\A[a-zA-Z_:]}
|
44
41
|
NON_ELEMENT_CHAR_RE = %r{[^\w:.-]}
|
@@ -104,11 +101,7 @@ module Athena
|
|
104
101
|
|
105
102
|
def wrap(out = nil)
|
106
103
|
res = nil
|
107
|
-
|
108
|
-
builder(:target => out).database {
|
109
|
-
res = super()
|
110
|
-
}
|
111
|
-
|
104
|
+
builder(:target => out).database { res = super() }
|
112
105
|
res
|
113
106
|
end
|
114
107
|
|
@@ -184,89 +177,44 @@ module Athena
|
|
184
177
|
spec.default!(prev_spec)
|
185
178
|
}
|
186
179
|
|
187
|
-
verbose(:spec, BaseSpec) { spec.inspect_spec }
|
188
|
-
|
189
180
|
XMLStreamin::XMLStreamListener.new(spec)
|
190
181
|
end
|
191
182
|
|
192
183
|
def define_spec(element, field, config, arg)
|
193
184
|
spec = ElementSpec.new(element, field, config)
|
194
|
-
|
195
|
-
case arg
|
196
|
-
when Hash then spec.specs!(arg)
|
197
|
-
else spec.default!(SubElementSpec.new(spec))
|
198
|
-
end
|
199
|
-
|
185
|
+
arg.is_a?(Hash) ? spec.specs!(arg) : spec.default!(SubElementSpec.new(spec))
|
200
186
|
spec
|
201
187
|
end
|
202
188
|
|
203
189
|
def merge_specs(container, key, spec)
|
204
|
-
container.insert!(key => spec) { |_,
|
205
|
-
if
|
206
|
-
|
207
|
-
|
190
|
+
container.insert!(key => spec) { |_, spec1, spec2|
|
191
|
+
if spec1.respond_to?(:specs!)
|
192
|
+
spec1.specs!(spec2.respond_to?(:specs) ? spec2.specs : spec2)
|
193
|
+
spec1
|
208
194
|
else
|
209
|
-
|
195
|
+
spec1.merge(spec2)
|
210
196
|
end
|
211
197
|
}
|
212
198
|
end
|
213
199
|
|
214
200
|
class BaseSpec < XMLStreamin::XMLSpec
|
215
201
|
|
216
|
-
include Util
|
217
|
-
|
218
|
-
@level = 0
|
219
|
-
|
220
202
|
def start(context, name, attrs)
|
221
|
-
verbose(:xml) {
|
222
|
-
spit "#{indent(level)}<#{name}>"; step :down
|
223
|
-
attrs.each { |attr| spit "#{indent(level + 1)}[#{attr[0]} = #{attr[1]}]" }
|
224
|
-
}
|
225
|
-
|
226
203
|
context
|
227
204
|
end
|
228
205
|
|
229
206
|
def text(context, data)
|
230
|
-
verbose(:xml) { spit "#{indent(level)}#{data.strip}" unless data.strip.empty? }
|
231
207
|
context
|
232
208
|
end
|
233
209
|
|
234
210
|
def done(context, name)
|
235
|
-
verbose(:xml) { step :up; spit "#{indent(level)}</#{name}>" }
|
236
211
|
context
|
237
212
|
end
|
238
213
|
|
239
214
|
def empty(context)
|
240
|
-
verbose(:xml) { step :up }
|
241
215
|
context
|
242
216
|
end
|
243
217
|
|
244
|
-
def inspect_spec(element = nil, level = 0)
|
245
|
-
if respond_to?(:field)
|
246
|
-
msg = "#{indent(level)}[#{element}] #{field.to_s.upcase} -> #{name}"
|
247
|
-
respond_to?(:spit) ? spit(msg) : warn(msg)
|
248
|
-
|
249
|
-
inspect_specs(level + 1)
|
250
|
-
else
|
251
|
-
specs.empty? ? specs.default.inspect_spec('?', level) : inspect_specs(level)
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
private
|
256
|
-
|
257
|
-
def inspect_specs(level = 0)
|
258
|
-
specs.each { |element, spec| spec.inspect_spec(element, level) }
|
259
|
-
end
|
260
|
-
|
261
|
-
def level
|
262
|
-
BaseSpec.instance_variable_get(:@level)
|
263
|
-
end
|
264
|
-
|
265
|
-
def step(direction)
|
266
|
-
steps = { :down => 1, :up => -1 }
|
267
|
-
BaseSpec.instance_variable_set(:@level, level + steps[direction])
|
268
|
-
end
|
269
|
-
|
270
218
|
end
|
271
219
|
|
272
220
|
class RecordSpec < BaseSpec
|
@@ -276,13 +224,12 @@ module Athena
|
|
276
224
|
|
277
225
|
def initialize(&block)
|
278
226
|
super()
|
279
|
-
|
280
227
|
@block = block
|
281
228
|
end
|
282
229
|
|
283
230
|
def start(context, name, attrs)
|
284
231
|
context = super
|
285
|
-
self.record = Record.new(nil, block, true)
|
232
|
+
self.record = Athena::Record.new(nil, block, true)
|
286
233
|
context
|
287
234
|
end
|
288
235
|
|
@@ -301,15 +248,12 @@ module Athena
|
|
301
248
|
|
302
249
|
def initialize(name, field, config)
|
303
250
|
super()
|
304
|
-
|
305
|
-
@name = name
|
306
|
-
@field = field
|
307
|
-
@config = config
|
251
|
+
@name, @field, @config = name, field, config
|
308
252
|
end
|
309
253
|
|
310
254
|
def start(context, name, attrs)
|
311
255
|
context = super
|
312
|
-
self.record = Record[field, config]
|
256
|
+
self.record = Athena::Record[field, config]
|
313
257
|
context
|
314
258
|
end
|
315
259
|
|
@@ -330,9 +274,7 @@ module Athena
|
|
330
274
|
|
331
275
|
def initialize(parent)
|
332
276
|
super()
|
333
|
-
|
334
277
|
@parent = parent
|
335
|
-
|
336
278
|
default!(self)
|
337
279
|
end
|
338
280
|
|
@@ -340,5 +282,4 @@ module Athena
|
|
340
282
|
|
341
283
|
end
|
342
284
|
|
343
|
-
end
|
344
285
|
end
|
data/lib/athena/parser.rb
CHANGED
@@ -26,65 +26,65 @@
|
|
26
26
|
###############################################################################
|
27
27
|
#++
|
28
28
|
|
29
|
-
|
30
|
-
class Parser
|
31
|
-
|
32
|
-
include Util
|
33
|
-
|
34
|
-
DEFAULT_SEPARATOR = ', '
|
35
|
-
DEFAULT_EMPTY = '<<EMPTY>>'
|
36
|
-
|
37
|
-
attr_reader :config, :spec
|
38
|
-
|
39
|
-
def initialize(config, spec)
|
40
|
-
@config = build_config(config)
|
41
|
-
@spec = Formats[:in, spec].new(self)
|
42
|
-
end
|
43
|
-
|
44
|
-
def parse(source, &block)
|
45
|
-
res = spec.parse(source, &block)
|
46
|
-
res.is_a?(Numeric) ? res : Record.records
|
47
|
-
end
|
29
|
+
require 'athena'
|
48
30
|
|
49
|
-
|
50
|
-
|
51
|
-
def build_config(config)
|
52
|
-
hash = {}
|
31
|
+
module Athena
|
53
32
|
|
54
|
-
|
55
|
-
if field.to_s =~ /\A__/
|
56
|
-
hash[field] = value
|
57
|
-
else
|
58
|
-
case value
|
59
|
-
when String, Array
|
60
|
-
elements, value = [*value], {}
|
61
|
-
when Hash
|
62
|
-
elements = value[:elements] || value[:element].to_a
|
33
|
+
class Parser
|
63
34
|
|
64
|
-
|
65
|
-
|
66
|
-
|
35
|
+
DEFAULT_SEPARATOR = ', '
|
36
|
+
DEFAULT_EMPTY = '<<EMPTY>>'
|
37
|
+
|
38
|
+
attr_reader :config, :spec
|
39
|
+
|
40
|
+
def initialize(config, spec)
|
41
|
+
@config = build_config(config)
|
42
|
+
@spec = Formats[:in, spec].new(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse(source, &block)
|
46
|
+
res = spec.parse(source, &block)
|
47
|
+
res.is_a?(Numeric) ? res : Record.records
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def build_config(config)
|
53
|
+
hash = {}
|
54
|
+
|
55
|
+
config.each { |field, value|
|
56
|
+
if field.to_s =~ /\A__/
|
57
|
+
hash[field] = value
|
58
|
+
else
|
59
|
+
case value
|
60
|
+
when String, Array
|
61
|
+
elements, value = [*value], {}
|
62
|
+
when Hash
|
63
|
+
elements = value[:elements] || value[:element].to_a
|
64
|
+
|
65
|
+
raise ArgumentError, "no elements specified for field #{field}" unless elements.is_a?(Array)
|
66
|
+
else
|
67
|
+
raise ArgumentError, "illegal value for field #{field}"
|
68
|
+
end
|
69
|
+
|
70
|
+
separator = value[:separator] || DEFAULT_SEPARATOR
|
71
|
+
|
72
|
+
elements.each { |element|
|
73
|
+
(hash[element] ||= {})[field] = {
|
74
|
+
:string => value[:string] || ['%s'] * elements.size * separator,
|
75
|
+
:empty => value[:empty] || DEFAULT_EMPTY,
|
76
|
+
:elements => elements
|
77
|
+
}
|
78
|
+
}
|
67
79
|
end
|
80
|
+
}
|
68
81
|
|
69
|
-
|
82
|
+
hash
|
83
|
+
end
|
70
84
|
|
71
|
-
|
72
|
-
|
85
|
+
class ConfigError < StandardError
|
86
|
+
end
|
73
87
|
|
74
|
-
(hash[element] ||= {})[field] = {
|
75
|
-
:string => value[:string] || ['%s'] * elements.size * separator,
|
76
|
-
:empty => value[:empty] || DEFAULT_EMPTY,
|
77
|
-
:elements => elements
|
78
|
-
}
|
79
|
-
}
|
80
|
-
end
|
81
|
-
}
|
82
|
-
|
83
|
-
hash
|
84
|
-
end
|
85
|
-
|
86
|
-
class ConfigError < StandardError
|
87
88
|
end
|
88
89
|
|
89
|
-
end
|
90
90
|
end
|
data/lib/athena/record.rb
CHANGED
@@ -27,80 +27,74 @@
|
|
27
27
|
#++
|
28
28
|
|
29
29
|
require 'nuggets/integer/map'
|
30
|
+
require 'athena'
|
30
31
|
|
31
32
|
module Athena
|
33
|
+
|
32
34
|
class Record
|
33
35
|
|
34
|
-
|
36
|
+
@records = []
|
35
37
|
|
36
|
-
|
38
|
+
class << self
|
37
39
|
|
38
|
-
|
40
|
+
def records
|
41
|
+
@records
|
42
|
+
end
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
def [](field = nil, config = nil)
|
45
|
+
record = records.last
|
46
|
+
raise NoRecordError unless record
|
43
47
|
|
44
|
-
|
45
|
-
|
46
|
-
|
48
|
+
record.fill(field, config) if field && config
|
49
|
+
record
|
50
|
+
end
|
47
51
|
|
48
|
-
record.fill(field, config) if field && config
|
49
|
-
record
|
50
52
|
end
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
-
attr_reader :struct, :block, :id
|
54
|
+
attr_reader :struct, :block, :id
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
def initialize(id = nil, block = nil, add = !block)
|
57
|
+
@id = id || object_id.map_positive
|
58
|
+
@block = block
|
59
|
+
@struct = {}
|
60
60
|
|
61
|
-
|
61
|
+
add_record if add
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
63
|
+
if block_given?
|
64
|
+
begin
|
65
|
+
yield self
|
66
|
+
ensure
|
67
|
+
close
|
68
|
+
end
|
68
69
|
end
|
69
70
|
end
|
70
|
-
end
|
71
71
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
def update(element, data, field_config = nil)
|
77
|
-
field_config.each { |field, config| fill(field, config) } if field_config
|
72
|
+
def fill(field, config)
|
73
|
+
struct[field] ||= config.merge(:values => Hash.new { |h, k| h[k] = [] })
|
74
|
+
end
|
78
75
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
76
|
+
def update(element, data, field_config = nil)
|
77
|
+
field_config.each { |field, config| fill(field, config) } if field_config
|
78
|
+
struct.each_key { |field| struct[field][:values][element] << data }
|
79
|
+
end
|
83
80
|
|
84
|
-
|
85
|
-
|
86
|
-
|
81
|
+
def close
|
82
|
+
block ? block[self] : self
|
83
|
+
end
|
87
84
|
|
88
|
-
|
89
|
-
|
90
|
-
|
85
|
+
def to(format)
|
86
|
+
Formats[:out, format].convert(self)
|
87
|
+
end
|
91
88
|
|
92
|
-
|
93
|
-
Formats[:out, format].convert(self)
|
94
|
-
end
|
89
|
+
private
|
95
90
|
|
96
|
-
|
91
|
+
def add_record
|
92
|
+
self.class.records << self
|
93
|
+
end
|
97
94
|
|
98
|
-
|
99
|
-
|
100
|
-
end
|
95
|
+
class NoRecordError < StandardError
|
96
|
+
end
|
101
97
|
|
102
|
-
class NoRecordError < StandardError
|
103
98
|
end
|
104
99
|
|
105
|
-
end
|
106
100
|
end
|
data/lib/athena/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: athena
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 21
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 2
|
8
9
|
- 1
|
9
|
-
|
10
|
-
version: 0.1.5
|
10
|
+
version: 0.2.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jens Wille
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-07-
|
18
|
+
date: 2011-07-27 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: builder
|
@@ -53,12 +53,12 @@ dependencies:
|
|
53
53
|
requirements:
|
54
54
|
- - ">="
|
55
55
|
- !ruby/object:Gem::Version
|
56
|
-
hash:
|
56
|
+
hash: 11
|
57
57
|
segments:
|
58
58
|
- 0
|
59
|
-
-
|
59
|
+
- 7
|
60
60
|
- 4
|
61
|
-
version: 0.
|
61
|
+
version: 0.7.4
|
62
62
|
type: :runtime
|
63
63
|
version_requirements: *id003
|
64
64
|
description: Convert database files to various formats.
|
@@ -72,8 +72,8 @@ extra_rdoc_files:
|
|
72
72
|
- COPYING
|
73
73
|
- ChangeLog
|
74
74
|
files:
|
75
|
-
- lib/athena/util.rb
|
76
75
|
- lib/athena/record.rb
|
76
|
+
- lib/athena/cli.rb
|
77
77
|
- lib/athena/formats/xml.rb
|
78
78
|
- lib/athena/formats/lingo.rb
|
79
79
|
- lib/athena/formats/ferret.rb
|
@@ -99,14 +99,14 @@ licenses: []
|
|
99
99
|
|
100
100
|
post_install_message:
|
101
101
|
rdoc_options:
|
102
|
+
- --all
|
103
|
+
- --main
|
104
|
+
- README
|
102
105
|
- --charset
|
103
106
|
- UTF-8
|
104
107
|
- --title
|
105
|
-
- athena Application documentation (v0.1
|
106
|
-
- --main
|
107
|
-
- README
|
108
|
+
- athena Application documentation (v0.2.1)
|
108
109
|
- --line-numbers
|
109
|
-
- --all
|
110
110
|
require_paths:
|
111
111
|
- lib
|
112
112
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -130,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
130
|
requirements: []
|
131
131
|
|
132
132
|
rubyforge_project: prometheus
|
133
|
-
rubygems_version: 1.8.
|
133
|
+
rubygems_version: 1.8.6
|
134
134
|
signing_key:
|
135
135
|
specification_version: 3
|
136
136
|
summary: Convert database files to various formats.
|
data/lib/athena/util.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
#--
|
2
|
-
###############################################################################
|
3
|
-
# #
|
4
|
-
# A component of athena, the database file converter. #
|
5
|
-
# #
|
6
|
-
# Copyright (C) 2007-2011 University of Cologne, #
|
7
|
-
# Albertus-Magnus-Platz, #
|
8
|
-
# 50923 Cologne, Germany #
|
9
|
-
# #
|
10
|
-
# Authors: #
|
11
|
-
# Jens Wille <jens.wille@uni-koeln.de> #
|
12
|
-
# #
|
13
|
-
# athena is free software; you can redistribute it and/or modify it under the #
|
14
|
-
# terms of the GNU Affero General Public License as published by the Free #
|
15
|
-
# Software Foundation; either version 3 of the License, or (at your option) #
|
16
|
-
# any later version. #
|
17
|
-
# #
|
18
|
-
# athena is distributed in the hope that it will be useful, but WITHOUT ANY #
|
19
|
-
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
20
|
-
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
|
21
|
-
# more details. #
|
22
|
-
# #
|
23
|
-
# You should have received a copy of the GNU Affero General Public License #
|
24
|
-
# along with athena. If not, see <http://www.gnu.org/licenses/>. #
|
25
|
-
# #
|
26
|
-
###############################################################################
|
27
|
-
#++
|
28
|
-
|
29
|
-
module Athena
|
30
|
-
module Util
|
31
|
-
|
32
|
-
extend self
|
33
|
-
|
34
|
-
def verbose(what, klass = self.class, &block)
|
35
|
-
if $Verbose && $Verbose[what]
|
36
|
-
klass.send(:define_method, :spit) { |msg|
|
37
|
-
warn "*#{what}: #{msg}"
|
38
|
-
}
|
39
|
-
|
40
|
-
klass.send(:define_method, :indent) { |*level|
|
41
|
-
' ' * (level.first || 0)
|
42
|
-
}
|
43
|
-
|
44
|
-
instance_eval(&block)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
end
|
49
|
-
end
|