activesupport 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activesupport might be problematic. Click here for more details.
- data/CHANGELOG +232 -2
- data/README +43 -0
- data/lib/active_support.rb +4 -1
- data/lib/active_support/breakpoint.rb +5 -0
- data/lib/active_support/core_ext/array.rb +2 -16
- data/lib/active_support/core_ext/array/conversions.rb +30 -4
- data/lib/active_support/core_ext/array/grouping.rb +55 -0
- data/lib/active_support/core_ext/bigdecimal.rb +3 -0
- data/lib/active_support/core_ext/bigdecimal/formatting.rb +7 -0
- data/lib/active_support/core_ext/class/inheritable_attributes.rb +6 -1
- data/lib/active_support/core_ext/date/conversions.rb +13 -7
- data/lib/active_support/core_ext/enumerable.rb +41 -10
- data/lib/active_support/core_ext/exception.rb +2 -2
- data/lib/active_support/core_ext/hash/conversions.rb +123 -12
- data/lib/active_support/core_ext/hash/indifferent_access.rb +18 -9
- data/lib/active_support/core_ext/integer/inflections.rb +10 -4
- data/lib/active_support/core_ext/load_error.rb +3 -3
- data/lib/active_support/core_ext/module.rb +2 -0
- data/lib/active_support/core_ext/module/aliasing.rb +58 -0
- data/lib/active_support/core_ext/module/attr_internal.rb +31 -0
- data/lib/active_support/core_ext/module/delegation.rb +27 -2
- data/lib/active_support/core_ext/name_error.rb +20 -0
- data/lib/active_support/core_ext/string.rb +2 -0
- data/lib/active_support/core_ext/string/access.rb +5 -5
- data/lib/active_support/core_ext/string/inflections.rb +93 -4
- data/lib/active_support/core_ext/string/unicode.rb +42 -0
- data/lib/active_support/core_ext/symbol.rb +1 -1
- data/lib/active_support/core_ext/time/calculations.rb +7 -5
- data/lib/active_support/core_ext/time/conversions.rb +1 -2
- data/lib/active_support/dependencies.rb +417 -50
- data/lib/active_support/deprecation.rb +201 -0
- data/lib/active_support/inflections.rb +1 -2
- data/lib/active_support/inflector.rb +117 -19
- data/lib/active_support/json.rb +14 -3
- data/lib/active_support/json/encoders/core.rb +21 -18
- data/lib/active_support/multibyte.rb +7 -0
- data/lib/active_support/multibyte/chars.rb +129 -0
- data/lib/active_support/multibyte/generators/generate_tables.rb +149 -0
- data/lib/active_support/multibyte/handlers/passthru_handler.rb +9 -0
- data/lib/active_support/multibyte/handlers/utf8_handler.rb +453 -0
- data/lib/active_support/multibyte/handlers/utf8_handler_proc.rb +44 -0
- data/lib/active_support/option_merger.rb +3 -3
- data/lib/active_support/ordered_options.rb +24 -23
- data/lib/active_support/reloadable.rb +39 -5
- data/lib/active_support/values/time_zone.rb +1 -1
- data/lib/active_support/values/unicode_tables.dat +0 -0
- data/lib/active_support/vendor/builder/blankslate.rb +16 -6
- data/lib/active_support/vendor/builder/xchar.rb +112 -0
- data/lib/active_support/vendor/builder/xmlbase.rb +12 -10
- data/lib/active_support/vendor/builder/xmlmarkup.rb +26 -7
- data/lib/active_support/vendor/xml_simple.rb +1021 -0
- data/lib/active_support/version.rb +2 -2
- data/lib/active_support/whiny_nil.rb +1 -1
- metadata +26 -4
- data/lib/active_support/core_ext/hash/conversions.rb.rej +0 -28
@@ -0,0 +1,1021 @@
|
|
1
|
+
# = XmlSimple
|
2
|
+
#
|
3
|
+
# Author:: Maik Schmidt <contact@maik-schmidt.de>
|
4
|
+
# Copyright:: Copyright (c) 2003-2006 Maik Schmidt
|
5
|
+
# License:: Distributes under the same terms as Ruby.
|
6
|
+
#
|
7
|
+
require 'rexml/document'
|
8
|
+
require 'stringio'
|
9
|
+
|
10
|
+
# Easy API to maintain XML (especially configuration files).
|
11
|
+
class XmlSimple
|
12
|
+
include REXML
|
13
|
+
|
14
|
+
@@VERSION = '1.0.9'
|
15
|
+
|
16
|
+
# A simple cache for XML documents that were already transformed
|
17
|
+
# by xml_in.
|
18
|
+
class Cache
|
19
|
+
# Creates and initializes a new Cache object.
|
20
|
+
def initialize
|
21
|
+
@mem_share_cache = {}
|
22
|
+
@mem_copy_cache = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Saves a data structure into a file.
|
26
|
+
#
|
27
|
+
# data::
|
28
|
+
# Data structure to be saved.
|
29
|
+
# filename::
|
30
|
+
# Name of the file belonging to the data structure.
|
31
|
+
def save_storable(data, filename)
|
32
|
+
cache_file = get_cache_filename(filename)
|
33
|
+
File.open(cache_file, "w+") { |f| Marshal.dump(data, f) }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Restores a data structure from a file. If restoring the data
|
37
|
+
# structure failed for any reason, nil will be returned.
|
38
|
+
#
|
39
|
+
# filename::
|
40
|
+
# Name of the file belonging to the data structure.
|
41
|
+
def restore_storable(filename)
|
42
|
+
cache_file = get_cache_filename(filename)
|
43
|
+
return nil unless File::exist?(cache_file)
|
44
|
+
return nil unless File::mtime(cache_file).to_i > File::mtime(filename).to_i
|
45
|
+
data = nil
|
46
|
+
File.open(cache_file) { |f| data = Marshal.load(f) }
|
47
|
+
data
|
48
|
+
end
|
49
|
+
|
50
|
+
# Saves a data structure in a shared memory cache.
|
51
|
+
#
|
52
|
+
# data::
|
53
|
+
# Data structure to be saved.
|
54
|
+
# filename::
|
55
|
+
# Name of the file belonging to the data structure.
|
56
|
+
def save_mem_share(data, filename)
|
57
|
+
@mem_share_cache[filename] = [Time::now.to_i, data]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Restores a data structure from a shared memory cache. You
|
61
|
+
# should consider these elements as "read only". If restoring
|
62
|
+
# the data structure failed for any reason, nil will be
|
63
|
+
# returned.
|
64
|
+
#
|
65
|
+
# filename::
|
66
|
+
# Name of the file belonging to the data structure.
|
67
|
+
def restore_mem_share(filename)
|
68
|
+
get_from_memory_cache(filename, @mem_share_cache)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Copies a data structure to a memory cache.
|
72
|
+
#
|
73
|
+
# data::
|
74
|
+
# Data structure to be copied.
|
75
|
+
# filename::
|
76
|
+
# Name of the file belonging to the data structure.
|
77
|
+
def save_mem_copy(data, filename)
|
78
|
+
@mem_share_cache[filename] = [Time::now.to_i, Marshal.dump(data)]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Restores a data structure from a memory cache. If restoring
|
82
|
+
# the data structure failed for any reason, nil will be
|
83
|
+
# returned.
|
84
|
+
#
|
85
|
+
# filename::
|
86
|
+
# Name of the file belonging to the data structure.
|
87
|
+
def restore_mem_copy(filename)
|
88
|
+
data = get_from_memory_cache(filename, @mem_share_cache)
|
89
|
+
data = Marshal.load(data) unless data.nil?
|
90
|
+
data
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Returns the "cache filename" belonging to a filename, i.e.
|
96
|
+
# the extension '.xml' in the original filename will be replaced
|
97
|
+
# by '.stor'. If filename does not have this extension, '.stor'
|
98
|
+
# will be appended.
|
99
|
+
#
|
100
|
+
# filename::
|
101
|
+
# Filename to get "cache filename" for.
|
102
|
+
def get_cache_filename(filename)
|
103
|
+
filename.sub(/(\.xml)?$/, '.stor')
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns a cache entry from a memory cache belonging to a
|
107
|
+
# certain filename. If no entry could be found for any reason,
|
108
|
+
# nil will be returned.
|
109
|
+
#
|
110
|
+
# filename::
|
111
|
+
# Name of the file the cache entry belongs to.
|
112
|
+
# cache::
|
113
|
+
# Memory cache to get entry from.
|
114
|
+
def get_from_memory_cache(filename, cache)
|
115
|
+
return nil unless cache[filename]
|
116
|
+
return nil unless cache[filename][0] > File::mtime(filename).to_i
|
117
|
+
return cache[filename][1]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Create a "global" cache.
|
122
|
+
@@cache = Cache.new
|
123
|
+
|
124
|
+
# Creates and intializes a new XmlSimple object.
|
125
|
+
#
|
126
|
+
# defaults::
|
127
|
+
# Default values for options.
|
128
|
+
def initialize(defaults = nil)
|
129
|
+
unless defaults.nil? || defaults.instance_of?(Hash)
|
130
|
+
raise ArgumentError, "Options have to be a Hash."
|
131
|
+
end
|
132
|
+
@default_options = normalize_option_names(defaults, KNOWN_OPTIONS['in'] & KNOWN_OPTIONS['out'])
|
133
|
+
@options = Hash.new
|
134
|
+
@_var_values = nil
|
135
|
+
end
|
136
|
+
|
137
|
+
# Converts an XML document in the same way as the Perl module XML::Simple.
|
138
|
+
#
|
139
|
+
# string::
|
140
|
+
# XML source. Could be one of the following:
|
141
|
+
#
|
142
|
+
# - nil: Tries to load and parse '<scriptname>.xml'.
|
143
|
+
# - filename: Tries to load and parse filename.
|
144
|
+
# - IO object: Reads from object until EOF is detected and parses result.
|
145
|
+
# - XML string: Parses string.
|
146
|
+
#
|
147
|
+
# options::
|
148
|
+
# Options to be used.
|
149
|
+
def xml_in(string = nil, options = nil)
|
150
|
+
handle_options('in', options)
|
151
|
+
|
152
|
+
# If no XML string or filename was supplied look for scriptname.xml.
|
153
|
+
if string.nil?
|
154
|
+
string = File::basename($0)
|
155
|
+
string.sub!(/\.[^.]+$/, '')
|
156
|
+
string += '.xml'
|
157
|
+
|
158
|
+
directory = File::dirname($0)
|
159
|
+
@options['searchpath'].unshift(directory) unless directory.nil?
|
160
|
+
end
|
161
|
+
|
162
|
+
if string.instance_of?(String)
|
163
|
+
if string =~ /<.*?>/m
|
164
|
+
@doc = parse(string)
|
165
|
+
elsif string == '-'
|
166
|
+
@doc = parse($stdin.readlines.to_s)
|
167
|
+
else
|
168
|
+
filename = find_xml_file(string, @options['searchpath'])
|
169
|
+
|
170
|
+
if @options.has_key?('cache')
|
171
|
+
@options['cache'].each { |scheme|
|
172
|
+
case(scheme)
|
173
|
+
when 'storable'
|
174
|
+
content = @@cache.restore_storable(filename)
|
175
|
+
when 'mem_share'
|
176
|
+
content = @@cache.restore_mem_share(filename)
|
177
|
+
when 'mem_copy'
|
178
|
+
content = @@cache.restore_mem_copy(filename)
|
179
|
+
else
|
180
|
+
raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
|
181
|
+
end
|
182
|
+
return content if content
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
@doc = load_xml_file(filename)
|
187
|
+
end
|
188
|
+
elsif string.kind_of?(IO) || string.kind_of?(StringIO)
|
189
|
+
@doc = parse(string.readlines.to_s)
|
190
|
+
else
|
191
|
+
raise ArgumentError, "Could not parse object of type: <#{string.type}>."
|
192
|
+
end
|
193
|
+
|
194
|
+
result = collapse(@doc.root)
|
195
|
+
result = @options['keeproot'] ? merge({}, @doc.root.name, result) : result
|
196
|
+
put_into_cache(result, filename)
|
197
|
+
result
|
198
|
+
end
|
199
|
+
|
200
|
+
# This is the functional version of the instance method xml_in.
|
201
|
+
def XmlSimple.xml_in(string = nil, options = nil)
|
202
|
+
xml_simple = XmlSimple.new
|
203
|
+
xml_simple.xml_in(string, options)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Converts a data structure into an XML document.
|
207
|
+
#
|
208
|
+
# ref::
|
209
|
+
# Reference to data structure to be converted into XML.
|
210
|
+
# options::
|
211
|
+
# Options to be used.
|
212
|
+
def xml_out(ref, options = nil)
|
213
|
+
handle_options('out', options)
|
214
|
+
if ref.instance_of?(Array)
|
215
|
+
ref = { @options['anonymoustag'] => ref }
|
216
|
+
end
|
217
|
+
|
218
|
+
if @options['keeproot']
|
219
|
+
keys = ref.keys
|
220
|
+
if keys.size == 1
|
221
|
+
ref = ref[keys[0]]
|
222
|
+
@options['rootname'] = keys[0]
|
223
|
+
end
|
224
|
+
elsif @options['rootname'] == ''
|
225
|
+
if ref.instance_of?(Hash)
|
226
|
+
refsave = ref
|
227
|
+
ref = {}
|
228
|
+
refsave.each { |key, value|
|
229
|
+
if !scalar(value)
|
230
|
+
ref[key] = value
|
231
|
+
else
|
232
|
+
ref[key] = [ value.to_s ]
|
233
|
+
end
|
234
|
+
}
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
@ancestors = []
|
239
|
+
xml = value_to_xml(ref, @options['rootname'], '')
|
240
|
+
@ancestors = nil
|
241
|
+
|
242
|
+
if @options['xmldeclaration']
|
243
|
+
xml = @options['xmldeclaration'] + "\n" + xml
|
244
|
+
end
|
245
|
+
|
246
|
+
if @options.has_key?('outputfile')
|
247
|
+
if @options['outputfile'].kind_of?(IO)
|
248
|
+
return @options['outputfile'].write(xml)
|
249
|
+
else
|
250
|
+
File.open(@options['outputfile'], "w") { |file| file.write(xml) }
|
251
|
+
end
|
252
|
+
end
|
253
|
+
xml
|
254
|
+
end
|
255
|
+
|
256
|
+
# This is the functional version of the instance method xml_out.
|
257
|
+
def XmlSimple.xml_out(hash, options = nil)
|
258
|
+
xml_simple = XmlSimple.new
|
259
|
+
xml_simple.xml_out(hash, options)
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
# Declare options that are valid for xml_in and xml_out.
|
265
|
+
KNOWN_OPTIONS = {
|
266
|
+
'in' => %w(
|
267
|
+
keyattr keeproot forcecontent contentkey noattr
|
268
|
+
searchpath forcearray suppressempty anonymoustag
|
269
|
+
cache grouptags normalisespace normalizespace
|
270
|
+
variables varattr keytosymbol
|
271
|
+
),
|
272
|
+
'out' => %w(
|
273
|
+
keyattr keeproot contentkey noattr rootname
|
274
|
+
xmldeclaration outputfile noescape suppressempty
|
275
|
+
anonymoustag indent grouptags noindent
|
276
|
+
)
|
277
|
+
}
|
278
|
+
|
279
|
+
# Define some reasonable defaults.
|
280
|
+
DEF_KEY_ATTRIBUTES = []
|
281
|
+
DEF_ROOT_NAME = 'opt'
|
282
|
+
DEF_CONTENT_KEY = 'content'
|
283
|
+
DEF_XML_DECLARATION = "<?xml version='1.0' standalone='yes'?>"
|
284
|
+
DEF_ANONYMOUS_TAG = 'anon'
|
285
|
+
DEF_FORCE_ARRAY = true
|
286
|
+
DEF_INDENTATION = ' '
|
287
|
+
DEF_KEY_TO_SYMBOL = false
|
288
|
+
|
289
|
+
# Normalizes option names in a hash, i.e., turns all
|
290
|
+
# characters to lower case and removes all underscores.
|
291
|
+
# Additionally, this method checks, if an unknown option
|
292
|
+
# was used and raises an according exception.
|
293
|
+
#
|
294
|
+
# options::
|
295
|
+
# Hash to be normalized.
|
296
|
+
# known_options::
|
297
|
+
# List of known options.
|
298
|
+
def normalize_option_names(options, known_options)
|
299
|
+
return nil if options.nil?
|
300
|
+
result = Hash.new
|
301
|
+
options.each { |key, value|
|
302
|
+
lkey = key.downcase
|
303
|
+
lkey.gsub!(/_/, '')
|
304
|
+
if !known_options.member?(lkey)
|
305
|
+
raise ArgumentError, "Unrecognised option: #{lkey}."
|
306
|
+
end
|
307
|
+
result[lkey] = value
|
308
|
+
}
|
309
|
+
result
|
310
|
+
end
|
311
|
+
|
312
|
+
# Merges a set of options with the default options.
|
313
|
+
#
|
314
|
+
# direction::
|
315
|
+
# 'in': If options should be handled for xml_in.
|
316
|
+
# 'out': If options should be handled for xml_out.
|
317
|
+
# options::
|
318
|
+
# Options to be merged with the default options.
|
319
|
+
def handle_options(direction, options)
|
320
|
+
@options = options || Hash.new
|
321
|
+
|
322
|
+
raise ArgumentError, "Options must be a Hash!" unless @options.instance_of?(Hash)
|
323
|
+
|
324
|
+
unless KNOWN_OPTIONS.has_key?(direction)
|
325
|
+
raise ArgumentError, "Unknown direction: <#{direction}>."
|
326
|
+
end
|
327
|
+
|
328
|
+
known_options = KNOWN_OPTIONS[direction]
|
329
|
+
@options = normalize_option_names(@options, known_options)
|
330
|
+
|
331
|
+
unless @default_options.nil?
|
332
|
+
known_options.each { |option|
|
333
|
+
unless @options.has_key?(option)
|
334
|
+
if @default_options.has_key?(option)
|
335
|
+
@options[option] = @default_options[option]
|
336
|
+
end
|
337
|
+
end
|
338
|
+
}
|
339
|
+
end
|
340
|
+
|
341
|
+
unless @options.has_key?('noattr')
|
342
|
+
@options['noattr'] = false
|
343
|
+
end
|
344
|
+
|
345
|
+
if @options.has_key?('rootname')
|
346
|
+
@options['rootname'] = '' if @options['rootname'].nil?
|
347
|
+
else
|
348
|
+
@options['rootname'] = DEF_ROOT_NAME
|
349
|
+
end
|
350
|
+
|
351
|
+
if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true
|
352
|
+
@options['xmldeclaration'] = DEF_XML_DECLARATION
|
353
|
+
end
|
354
|
+
|
355
|
+
@options['keytosymbol'] = DEF_KEY_TO_SYMBOL unless @options.has_key?('keytosymbol')
|
356
|
+
|
357
|
+
if @options.has_key?('contentkey')
|
358
|
+
if @options['contentkey'] =~ /^-(.*)$/
|
359
|
+
@options['contentkey'] = $1
|
360
|
+
@options['collapseagain'] = true
|
361
|
+
end
|
362
|
+
else
|
363
|
+
@options['contentkey'] = DEF_CONTENT_KEY
|
364
|
+
end
|
365
|
+
|
366
|
+
unless @options.has_key?('normalisespace')
|
367
|
+
@options['normalisespace'] = @options['normalizespace']
|
368
|
+
end
|
369
|
+
@options['normalisespace'] = 0 if @options['normalisespace'].nil?
|
370
|
+
|
371
|
+
if @options.has_key?('searchpath')
|
372
|
+
unless @options['searchpath'].instance_of?(Array)
|
373
|
+
@options['searchpath'] = [ @options['searchpath'] ]
|
374
|
+
end
|
375
|
+
else
|
376
|
+
@options['searchpath'] = []
|
377
|
+
end
|
378
|
+
|
379
|
+
if @options.has_key?('cache') && scalar(@options['cache'])
|
380
|
+
@options['cache'] = [ @options['cache'] ]
|
381
|
+
end
|
382
|
+
|
383
|
+
@options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag')
|
384
|
+
|
385
|
+
if !@options.has_key?('indent') || @options['indent'].nil?
|
386
|
+
@options['indent'] = DEF_INDENTATION
|
387
|
+
end
|
388
|
+
|
389
|
+
@options['indent'] = '' if @options.has_key?('noindent')
|
390
|
+
|
391
|
+
# Special cleanup for 'keyattr' which could be an array or
|
392
|
+
# a hash or left to default to array.
|
393
|
+
if @options.has_key?('keyattr')
|
394
|
+
if !scalar(@options['keyattr'])
|
395
|
+
# Convert keyattr => { elem => '+attr' }
|
396
|
+
# to keyattr => { elem => ['attr', '+'] }
|
397
|
+
if @options['keyattr'].instance_of?(Hash)
|
398
|
+
@options['keyattr'].each { |key, value|
|
399
|
+
if value =~ /^([-+])?(.*)$/
|
400
|
+
@options['keyattr'][key] = [$2, $1 ? $1 : '']
|
401
|
+
end
|
402
|
+
}
|
403
|
+
elsif !@options['keyattr'].instance_of?(Array)
|
404
|
+
raise ArgumentError, "'keyattr' must be String, Hash, or Array!"
|
405
|
+
end
|
406
|
+
else
|
407
|
+
@options['keyattr'] = [ @options['keyattr'] ]
|
408
|
+
end
|
409
|
+
else
|
410
|
+
@options['keyattr'] = DEF_KEY_ATTRIBUTES
|
411
|
+
end
|
412
|
+
|
413
|
+
if @options.has_key?('forcearray')
|
414
|
+
if @options['forcearray'].instance_of?(Regexp)
|
415
|
+
@options['forcearray'] = [ @options['forcearray'] ]
|
416
|
+
end
|
417
|
+
|
418
|
+
if @options['forcearray'].instance_of?(Array)
|
419
|
+
force_list = @options['forcearray']
|
420
|
+
unless force_list.empty?
|
421
|
+
@options['forcearray'] = {}
|
422
|
+
force_list.each { |tag|
|
423
|
+
if tag.instance_of?(Regexp)
|
424
|
+
unless @options['forcearray']['_regex'].instance_of?(Array)
|
425
|
+
@options['forcearray']['_regex'] = []
|
426
|
+
end
|
427
|
+
@options['forcearray']['_regex'] << tag
|
428
|
+
else
|
429
|
+
@options['forcearray'][tag] = true
|
430
|
+
end
|
431
|
+
}
|
432
|
+
else
|
433
|
+
@options['forcearray'] = false
|
434
|
+
end
|
435
|
+
else
|
436
|
+
@options['forcearray'] = @options['forcearray'] ? true : false
|
437
|
+
end
|
438
|
+
else
|
439
|
+
@options['forcearray'] = DEF_FORCE_ARRAY
|
440
|
+
end
|
441
|
+
|
442
|
+
if @options.has_key?('grouptags') && !@options['grouptags'].instance_of?(Hash)
|
443
|
+
raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash."
|
444
|
+
end
|
445
|
+
|
446
|
+
if @options.has_key?('variables') && !@options['variables'].instance_of?(Hash)
|
447
|
+
raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash."
|
448
|
+
end
|
449
|
+
|
450
|
+
if @options.has_key?('variables')
|
451
|
+
@_var_values = @options['variables']
|
452
|
+
elsif @options.has_key?('varattr')
|
453
|
+
@_var_values = {}
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# Actually converts an XML document element into a data structure.
|
458
|
+
#
|
459
|
+
# element::
|
460
|
+
# The document element to be collapsed.
|
461
|
+
def collapse(element)
|
462
|
+
result = @options['noattr'] ? {} : get_attributes(element)
|
463
|
+
|
464
|
+
if @options['normalisespace'] == 2
|
465
|
+
result.each { |k, v| result[k] = normalise_space(v) }
|
466
|
+
end
|
467
|
+
|
468
|
+
if element.has_elements?
|
469
|
+
element.each_element { |child|
|
470
|
+
value = collapse(child)
|
471
|
+
if empty(value) && (element.attributes.empty? || @options['noattr'])
|
472
|
+
next if @options.has_key?('suppressempty') && @options['suppressempty'] == true
|
473
|
+
end
|
474
|
+
result = merge(result, child.name, value)
|
475
|
+
}
|
476
|
+
if has_mixed_content?(element)
|
477
|
+
# normalisespace?
|
478
|
+
content = element.texts.map { |x| x.to_s }
|
479
|
+
content = content[0] if content.size == 1
|
480
|
+
result[@options['contentkey']] = content
|
481
|
+
end
|
482
|
+
elsif element.has_text? # i.e. it has only text.
|
483
|
+
return collapse_text_node(result, element)
|
484
|
+
end
|
485
|
+
|
486
|
+
# Turn Arrays into Hashes if key fields present.
|
487
|
+
count = fold_arrays(result)
|
488
|
+
|
489
|
+
# Disintermediate grouped tags.
|
490
|
+
if @options.has_key?('grouptags')
|
491
|
+
result.each { |key, value|
|
492
|
+
next unless (value.instance_of?(Hash) && (value.size == 1))
|
493
|
+
child_key, child_value = value.to_a[0]
|
494
|
+
if @options['grouptags'][key] == child_key
|
495
|
+
result[key] = child_value
|
496
|
+
end
|
497
|
+
}
|
498
|
+
end
|
499
|
+
|
500
|
+
# Fold Hases containing a single anonymous Array up into just the Array.
|
501
|
+
if count == 1
|
502
|
+
anonymoustag = @options['anonymoustag']
|
503
|
+
if result.has_key?(anonymoustag) && result[anonymoustag].instance_of?(Array)
|
504
|
+
return result[anonymoustag]
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
if result.empty? && @options.has_key?('suppressempty')
|
509
|
+
return @options['suppressempty'] == '' ? '' : nil
|
510
|
+
end
|
511
|
+
|
512
|
+
result
|
513
|
+
end
|
514
|
+
|
515
|
+
# Collapses a text node and merges it with an existing Hash, if
|
516
|
+
# possible.
|
517
|
+
# Thanks to Curtis Schofield for reporting a subtle bug.
|
518
|
+
#
|
519
|
+
# hash::
|
520
|
+
# Hash to merge text node value with, if possible.
|
521
|
+
# element::
|
522
|
+
# Text node to be collapsed.
|
523
|
+
def collapse_text_node(hash, element)
|
524
|
+
value = node_to_text(element)
|
525
|
+
if empty(value) && !element.has_attributes?
|
526
|
+
return {}
|
527
|
+
end
|
528
|
+
|
529
|
+
if element.has_attributes? && !@options['noattr']
|
530
|
+
return merge(hash, @options['contentkey'], value)
|
531
|
+
else
|
532
|
+
if @options['forcecontent']
|
533
|
+
return merge(hash, @options['contentkey'], value)
|
534
|
+
else
|
535
|
+
return value
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# Folds all arrays in a Hash.
|
541
|
+
#
|
542
|
+
# hash::
|
543
|
+
# Hash to be folded.
|
544
|
+
def fold_arrays(hash)
|
545
|
+
fold_amount = 0
|
546
|
+
keyattr = @options['keyattr']
|
547
|
+
if (keyattr.instance_of?(Array) || keyattr.instance_of?(Hash))
|
548
|
+
hash.each { |key, value|
|
549
|
+
if value.instance_of?(Array)
|
550
|
+
if keyattr.instance_of?(Array)
|
551
|
+
hash[key] = fold_array(value)
|
552
|
+
else
|
553
|
+
hash[key] = fold_array_by_name(key, value)
|
554
|
+
end
|
555
|
+
fold_amount += 1
|
556
|
+
end
|
557
|
+
}
|
558
|
+
end
|
559
|
+
fold_amount
|
560
|
+
end
|
561
|
+
|
562
|
+
# Folds an Array to a Hash, if possible. Folding happens
|
563
|
+
# according to the content of keyattr, which has to be
|
564
|
+
# an array.
|
565
|
+
#
|
566
|
+
# array::
|
567
|
+
# Array to be folded.
|
568
|
+
def fold_array(array)
|
569
|
+
hash = Hash.new
|
570
|
+
array.each { |x|
|
571
|
+
return array unless x.instance_of?(Hash)
|
572
|
+
key_matched = false
|
573
|
+
@options['keyattr'].each { |key|
|
574
|
+
if x.has_key?(key)
|
575
|
+
key_matched = true
|
576
|
+
value = x[key]
|
577
|
+
return array if value.instance_of?(Hash) || value.instance_of?(Array)
|
578
|
+
value = normalise_space(value) if @options['normalisespace'] == 1
|
579
|
+
x.delete(key)
|
580
|
+
hash[value] = x
|
581
|
+
break
|
582
|
+
end
|
583
|
+
}
|
584
|
+
return array unless key_matched
|
585
|
+
}
|
586
|
+
hash = collapse_content(hash) if @options['collapseagain']
|
587
|
+
hash
|
588
|
+
end
|
589
|
+
|
590
|
+
# Folds an Array to a Hash, if possible. Folding happens
|
591
|
+
# according to the content of keyattr, which has to be
|
592
|
+
# a Hash.
|
593
|
+
#
|
594
|
+
# name::
|
595
|
+
# Name of the attribute to be folded upon.
|
596
|
+
# array::
|
597
|
+
# Array to be folded.
|
598
|
+
def fold_array_by_name(name, array)
|
599
|
+
return array unless @options['keyattr'].has_key?(name)
|
600
|
+
key, flag = @options['keyattr'][name]
|
601
|
+
|
602
|
+
hash = Hash.new
|
603
|
+
array.each { |x|
|
604
|
+
if x.instance_of?(Hash) && x.has_key?(key)
|
605
|
+
value = x[key]
|
606
|
+
return array if value.instance_of?(Hash) || value.instance_of?(Array)
|
607
|
+
value = normalise_space(value) if @options['normalisespace'] == 1
|
608
|
+
hash[value] = x
|
609
|
+
hash[value]["-#{key}"] = hash[value][key] if flag == '-'
|
610
|
+
hash[value].delete(key) unless flag == '+'
|
611
|
+
else
|
612
|
+
$stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.")
|
613
|
+
return array
|
614
|
+
end
|
615
|
+
}
|
616
|
+
hash = collapse_content(hash) if @options['collapseagain']
|
617
|
+
hash
|
618
|
+
end
|
619
|
+
|
620
|
+
# Tries to collapse a Hash even more ;-)
|
621
|
+
#
|
622
|
+
# hash::
|
623
|
+
# Hash to be collapsed again.
|
624
|
+
def collapse_content(hash)
|
625
|
+
content_key = @options['contentkey']
|
626
|
+
hash.each_value { |value|
|
627
|
+
return hash unless value.instance_of?(Hash) && value.size == 1 && value.has_key?(content_key)
|
628
|
+
hash.each_key { |key| hash[key] = hash[key][content_key] }
|
629
|
+
}
|
630
|
+
hash
|
631
|
+
end
|
632
|
+
|
633
|
+
# Adds a new key/value pair to an existing Hash. If the key to be added
|
634
|
+
# does already exist and the existing value associated with key is not
|
635
|
+
# an Array, it will be converted into an Array. Then the new value is
|
636
|
+
# appended to that Array.
|
637
|
+
#
|
638
|
+
# hash::
|
639
|
+
# Hash to add key/value pair to.
|
640
|
+
# key::
|
641
|
+
# Key to be added.
|
642
|
+
# value::
|
643
|
+
# Value to be associated with key.
|
644
|
+
def merge(hash, key, value)
|
645
|
+
if value.instance_of?(String)
|
646
|
+
value = normalise_space(value) if @options['normalisespace'] == 2
|
647
|
+
|
648
|
+
# do variable substitutions
|
649
|
+
unless @_var_values.nil? || @_var_values.empty?
|
650
|
+
value.gsub!(/\$\{(\w+)\}/) { |x| get_var($1) }
|
651
|
+
end
|
652
|
+
|
653
|
+
# look for variable definitions
|
654
|
+
if @options.has_key?('varattr')
|
655
|
+
varattr = @options['varattr']
|
656
|
+
if hash.has_key?(varattr)
|
657
|
+
set_var(hash[varattr], value)
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
#patch for converting keys to symbols
|
663
|
+
if @options.has_key?('keytosymbol')
|
664
|
+
if @options['keytosymbol'] == true
|
665
|
+
key = key.to_s.downcase.to_sym
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
if hash.has_key?(key)
|
670
|
+
if hash[key].instance_of?(Array)
|
671
|
+
hash[key] << value
|
672
|
+
else
|
673
|
+
hash[key] = [ hash[key], value ]
|
674
|
+
end
|
675
|
+
elsif value.instance_of?(Array) # Handle anonymous arrays.
|
676
|
+
hash[key] = [ value ]
|
677
|
+
else
|
678
|
+
if force_array?(key)
|
679
|
+
hash[key] = [ value ]
|
680
|
+
else
|
681
|
+
hash[key] = value
|
682
|
+
end
|
683
|
+
end
|
684
|
+
hash
|
685
|
+
end
|
686
|
+
|
687
|
+
# Checks, if the 'forcearray' option has to be used for
|
688
|
+
# a certain key.
|
689
|
+
def force_array?(key)
|
690
|
+
return false if key == @options['contentkey']
|
691
|
+
return true if @options['forcearray'] == true
|
692
|
+
forcearray = @options['forcearray']
|
693
|
+
if forcearray.instance_of?(Hash)
|
694
|
+
return true if forcearray.has_key?(key)
|
695
|
+
return false unless forcearray.has_key?('_regex')
|
696
|
+
forcearray['_regex'].each { |x| return true if key =~ x }
|
697
|
+
end
|
698
|
+
return false
|
699
|
+
end
|
700
|
+
|
701
|
+
# Converts the attributes array of a document node into a Hash.
|
702
|
+
# Returns an empty Hash, if node has no attributes.
|
703
|
+
#
|
704
|
+
# node::
|
705
|
+
# Document node to extract attributes from.
|
706
|
+
def get_attributes(node)
|
707
|
+
attributes = {}
|
708
|
+
node.attributes.each { |n,v| attributes[n] = v }
|
709
|
+
attributes
|
710
|
+
end
|
711
|
+
|
712
|
+
# Determines, if a document element has mixed content.
|
713
|
+
#
|
714
|
+
# element::
|
715
|
+
# Document element to be checked.
|
716
|
+
def has_mixed_content?(element)
|
717
|
+
if element.has_text? && element.has_elements?
|
718
|
+
return true if element.texts.join('') !~ /^\s*$/s
|
719
|
+
end
|
720
|
+
false
|
721
|
+
end
|
722
|
+
|
723
|
+
# Called when a variable definition is encountered in the XML.
|
724
|
+
# A variable definition looks like
|
725
|
+
# <element attrname="name">value</element>
|
726
|
+
# where attrname matches the varattr setting.
|
727
|
+
def set_var(name, value)
|
728
|
+
@_var_values[name] = value
|
729
|
+
end
|
730
|
+
|
731
|
+
# Called during variable substitution to get the value for the
|
732
|
+
# named variable.
|
733
|
+
def get_var(name)
|
734
|
+
if @_var_values.has_key?(name)
|
735
|
+
return @_var_values[name]
|
736
|
+
else
|
737
|
+
return "${#{name}}"
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
# Recurses through a data structure building up and returning an
|
742
|
+
# XML representation of that structure as a string.
|
743
|
+
#
|
744
|
+
# ref::
|
745
|
+
# Reference to the data structure to be encoded.
|
746
|
+
# name::
|
747
|
+
# The XML tag name to be used for this item.
|
748
|
+
# indent::
|
749
|
+
# A string of spaces for use as the current indent level.
|
750
|
+
def value_to_xml(ref, name, indent)
|
751
|
+
named = !name.nil? && name != ''
|
752
|
+
nl = @options.has_key?('noindent') ? '' : "\n"
|
753
|
+
|
754
|
+
if !scalar(ref)
|
755
|
+
if @ancestors.member?(ref)
|
756
|
+
raise ArgumentError, "Circular data structures not supported!"
|
757
|
+
end
|
758
|
+
@ancestors << ref
|
759
|
+
else
|
760
|
+
if named
|
761
|
+
return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '</', name, '>', nl].join('')
|
762
|
+
else
|
763
|
+
return ref.to_s + nl
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
# Unfold hash to array if possible.
|
768
|
+
if ref.instance_of?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != ''
|
769
|
+
ref = hash_to_array(name, ref)
|
770
|
+
end
|
771
|
+
|
772
|
+
result = []
|
773
|
+
if ref.instance_of?(Hash)
|
774
|
+
# Reintermediate grouped values if applicable.
|
775
|
+
if @options.has_key?('grouptags')
|
776
|
+
ref.each { |key, value|
|
777
|
+
if @options['grouptags'].has_key?(key)
|
778
|
+
ref[key] = { @options['grouptags'][key] => value }
|
779
|
+
end
|
780
|
+
}
|
781
|
+
end
|
782
|
+
|
783
|
+
nested = []
|
784
|
+
text_content = nil
|
785
|
+
if named
|
786
|
+
result << indent << '<' << name
|
787
|
+
end
|
788
|
+
|
789
|
+
if !ref.empty?
|
790
|
+
ref.each { |key, value|
|
791
|
+
next if !key.nil? && key[0, 1] == '-'
|
792
|
+
if value.nil?
|
793
|
+
unless @options.has_key?('suppressempty') && @options['suppressempty'].nil?
|
794
|
+
raise ArgumentError, "Use of uninitialized value!"
|
795
|
+
end
|
796
|
+
value = {}
|
797
|
+
end
|
798
|
+
|
799
|
+
if !scalar(value) || @options['noattr']
|
800
|
+
nested << value_to_xml(value, key, indent + @options['indent'])
|
801
|
+
else
|
802
|
+
value = value.to_s
|
803
|
+
value = escape_value(value) unless @options['noescape']
|
804
|
+
if key == @options['contentkey']
|
805
|
+
text_content = value
|
806
|
+
else
|
807
|
+
result << ' ' << key << '="' << value << '"'
|
808
|
+
end
|
809
|
+
end
|
810
|
+
}
|
811
|
+
else
|
812
|
+
text_content = ''
|
813
|
+
end
|
814
|
+
|
815
|
+
if !nested.empty? || !text_content.nil?
|
816
|
+
if named
|
817
|
+
result << '>'
|
818
|
+
if !text_content.nil?
|
819
|
+
result << text_content
|
820
|
+
nested[0].sub!(/^\s+/, '') if !nested.empty?
|
821
|
+
else
|
822
|
+
result << nl
|
823
|
+
end
|
824
|
+
if !nested.empty?
|
825
|
+
result << nested << indent
|
826
|
+
end
|
827
|
+
result << '</' << name << '>' << nl
|
828
|
+
else
|
829
|
+
result << nested
|
830
|
+
end
|
831
|
+
else
|
832
|
+
result << ' />' << nl
|
833
|
+
end
|
834
|
+
elsif ref.instance_of?(Array)
|
835
|
+
ref.each { |value|
|
836
|
+
if scalar(value)
|
837
|
+
result << indent << '<' << name << '>'
|
838
|
+
result << (@options['noescape'] ? value.to_s : escape_value(value.to_s))
|
839
|
+
result << '</' << name << '>' << nl
|
840
|
+
elsif value.instance_of?(Hash)
|
841
|
+
result << value_to_xml(value, name, indent)
|
842
|
+
else
|
843
|
+
result << indent << '<' << name << '>' << nl
|
844
|
+
result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent'])
|
845
|
+
result << indent << '</' << name << '>' << nl
|
846
|
+
end
|
847
|
+
}
|
848
|
+
else
|
849
|
+
# Probably, this is obsolete.
|
850
|
+
raise ArgumentError, "Can't encode a value of type: #{ref.type}."
|
851
|
+
end
|
852
|
+
@ancestors.pop if !scalar(ref)
|
853
|
+
result.join('')
|
854
|
+
end
|
855
|
+
|
856
|
+
# Checks, if a certain value is a "scalar" value. Whatever
|
857
|
+
# that will be in Ruby ... ;-)
|
858
|
+
#
|
859
|
+
# value::
|
860
|
+
# Value to be checked.
|
861
|
+
def scalar(value)
|
862
|
+
return false if value.instance_of?(Hash) || value.instance_of?(Array)
|
863
|
+
return true
|
864
|
+
end
|
865
|
+
|
866
|
+
# Attempts to unfold a hash of hashes into an array of hashes. Returns
|
867
|
+
# a reference to th array on success or the original hash, if unfolding
|
868
|
+
# is not possible.
|
869
|
+
#
|
870
|
+
# parent::
|
871
|
+
#
|
872
|
+
# hashref::
|
873
|
+
# Reference to the hash to be unfolded.
|
874
|
+
def hash_to_array(parent, hashref)
|
875
|
+
arrayref = []
|
876
|
+
hashref.each { |key, value|
|
877
|
+
return hashref unless value.instance_of?(Hash)
|
878
|
+
|
879
|
+
if @options['keyattr'].instance_of?(Hash)
|
880
|
+
return hashref unless @options['keyattr'].has_key?(parent)
|
881
|
+
arrayref << { @options['keyattr'][parent][0] => key }.update(value)
|
882
|
+
else
|
883
|
+
arrayref << { @options['keyattr'][0] => key }.update(value)
|
884
|
+
end
|
885
|
+
}
|
886
|
+
arrayref
|
887
|
+
end
|
888
|
+
|
889
|
+
# Replaces XML markup characters by their external entities.
|
890
|
+
#
|
891
|
+
# data::
|
892
|
+
# The string to be escaped.
|
893
|
+
def escape_value(data)
|
894
|
+
Text::normalize(data)
|
895
|
+
end
|
896
|
+
|
897
|
+
# Removes leading and trailing whitespace and sequences of
|
898
|
+
# whitespaces from a string.
|
899
|
+
#
|
900
|
+
# text::
|
901
|
+
# String to be normalised.
|
902
|
+
def normalise_space(text)
|
903
|
+
text.strip.gsub(/\s\s+/, ' ')
|
904
|
+
end
|
905
|
+
|
906
|
+
# Checks, if an object is nil, an empty String or an empty Hash.
|
907
|
+
# Thanks to Norbert Gawor for a bugfix.
|
908
|
+
#
|
909
|
+
# value::
|
910
|
+
# Value to be checked for emptyness.
|
911
|
+
def empty(value)
|
912
|
+
case value
|
913
|
+
when Hash
|
914
|
+
return value.empty?
|
915
|
+
when String
|
916
|
+
return value !~ /\S/m
|
917
|
+
else
|
918
|
+
return value.nil?
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
# Converts a document node into a String.
|
923
|
+
# If the node could not be converted into a String
|
924
|
+
# for any reason, default will be returned.
|
925
|
+
#
|
926
|
+
# node::
|
927
|
+
# Document node to be converted.
|
928
|
+
# default::
|
929
|
+
# Value to be returned, if node could not be converted.
|
930
|
+
def node_to_text(node, default = nil)
|
931
|
+
if node.instance_of?(REXML::Element)
|
932
|
+
node.texts.map { |t| t.value }.join('')
|
933
|
+
elsif node.instance_of?(REXML::Attribute)
|
934
|
+
node.value.nil? ? default : node.value.strip
|
935
|
+
elsif node.instance_of?(REXML::Text)
|
936
|
+
node.value.strip
|
937
|
+
else
|
938
|
+
default
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
# Parses an XML string and returns the according document.
|
943
|
+
#
|
944
|
+
# xml_string::
|
945
|
+
# XML string to be parsed.
|
946
|
+
#
|
947
|
+
# The following exception may be raised:
|
948
|
+
#
|
949
|
+
# REXML::ParseException::
|
950
|
+
# If the specified file is not wellformed.
|
951
|
+
def parse(xml_string)
|
952
|
+
Document.new(xml_string)
|
953
|
+
end
|
954
|
+
|
955
|
+
# Searches in a list of paths for a certain file. Returns
|
956
|
+
# the full path to the file, if it could be found. Otherwise,
|
957
|
+
# an exception will be raised.
|
958
|
+
#
|
959
|
+
# filename::
|
960
|
+
# Name of the file to search for.
|
961
|
+
# searchpath::
|
962
|
+
# List of paths to search in.
|
963
|
+
def find_xml_file(file, searchpath)
|
964
|
+
filename = File::basename(file)
|
965
|
+
|
966
|
+
if filename != file
|
967
|
+
return file if File::file?(file)
|
968
|
+
else
|
969
|
+
searchpath.each { |path|
|
970
|
+
full_path = File::join(path, filename)
|
971
|
+
return full_path if File::file?(full_path)
|
972
|
+
}
|
973
|
+
end
|
974
|
+
|
975
|
+
if searchpath.empty?
|
976
|
+
return file if File::file?(file)
|
977
|
+
raise ArgumentError, "File does not exist: #{file}."
|
978
|
+
end
|
979
|
+
raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>"
|
980
|
+
end
|
981
|
+
|
982
|
+
# Loads and parses an XML configuration file.
|
983
|
+
#
|
984
|
+
# filename::
|
985
|
+
# Name of the configuration file to be loaded.
|
986
|
+
#
|
987
|
+
# The following exceptions may be raised:
|
988
|
+
#
|
989
|
+
# Errno::ENOENT::
|
990
|
+
# If the specified file does not exist.
|
991
|
+
# REXML::ParseException::
|
992
|
+
# If the specified file is not wellformed.
|
993
|
+
def load_xml_file(filename)
|
994
|
+
parse(File.readlines(filename).to_s)
|
995
|
+
end
|
996
|
+
|
997
|
+
# Caches the data belonging to a certain file.
|
998
|
+
#
|
999
|
+
# data::
|
1000
|
+
# Data to be cached.
|
1001
|
+
# filename::
|
1002
|
+
# Name of file the data was read from.
|
1003
|
+
def put_into_cache(data, filename)
|
1004
|
+
if @options.has_key?('cache')
|
1005
|
+
@options['cache'].each { |scheme|
|
1006
|
+
case(scheme)
|
1007
|
+
when 'storable'
|
1008
|
+
@@cache.save_storable(data, filename)
|
1009
|
+
when 'mem_share'
|
1010
|
+
@@cache.save_mem_share(data, filename)
|
1011
|
+
when 'mem_copy'
|
1012
|
+
@@cache.save_mem_copy(data, filename)
|
1013
|
+
else
|
1014
|
+
raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
|
1015
|
+
end
|
1016
|
+
}
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
end
|
1020
|
+
|
1021
|
+
# vim:sw=2
|