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.

Files changed (55) hide show
  1. data/CHANGELOG +232 -2
  2. data/README +43 -0
  3. data/lib/active_support.rb +4 -1
  4. data/lib/active_support/breakpoint.rb +5 -0
  5. data/lib/active_support/core_ext/array.rb +2 -16
  6. data/lib/active_support/core_ext/array/conversions.rb +30 -4
  7. data/lib/active_support/core_ext/array/grouping.rb +55 -0
  8. data/lib/active_support/core_ext/bigdecimal.rb +3 -0
  9. data/lib/active_support/core_ext/bigdecimal/formatting.rb +7 -0
  10. data/lib/active_support/core_ext/class/inheritable_attributes.rb +6 -1
  11. data/lib/active_support/core_ext/date/conversions.rb +13 -7
  12. data/lib/active_support/core_ext/enumerable.rb +41 -10
  13. data/lib/active_support/core_ext/exception.rb +2 -2
  14. data/lib/active_support/core_ext/hash/conversions.rb +123 -12
  15. data/lib/active_support/core_ext/hash/indifferent_access.rb +18 -9
  16. data/lib/active_support/core_ext/integer/inflections.rb +10 -4
  17. data/lib/active_support/core_ext/load_error.rb +3 -3
  18. data/lib/active_support/core_ext/module.rb +2 -0
  19. data/lib/active_support/core_ext/module/aliasing.rb +58 -0
  20. data/lib/active_support/core_ext/module/attr_internal.rb +31 -0
  21. data/lib/active_support/core_ext/module/delegation.rb +27 -2
  22. data/lib/active_support/core_ext/name_error.rb +20 -0
  23. data/lib/active_support/core_ext/string.rb +2 -0
  24. data/lib/active_support/core_ext/string/access.rb +5 -5
  25. data/lib/active_support/core_ext/string/inflections.rb +93 -4
  26. data/lib/active_support/core_ext/string/unicode.rb +42 -0
  27. data/lib/active_support/core_ext/symbol.rb +1 -1
  28. data/lib/active_support/core_ext/time/calculations.rb +7 -5
  29. data/lib/active_support/core_ext/time/conversions.rb +1 -2
  30. data/lib/active_support/dependencies.rb +417 -50
  31. data/lib/active_support/deprecation.rb +201 -0
  32. data/lib/active_support/inflections.rb +1 -2
  33. data/lib/active_support/inflector.rb +117 -19
  34. data/lib/active_support/json.rb +14 -3
  35. data/lib/active_support/json/encoders/core.rb +21 -18
  36. data/lib/active_support/multibyte.rb +7 -0
  37. data/lib/active_support/multibyte/chars.rb +129 -0
  38. data/lib/active_support/multibyte/generators/generate_tables.rb +149 -0
  39. data/lib/active_support/multibyte/handlers/passthru_handler.rb +9 -0
  40. data/lib/active_support/multibyte/handlers/utf8_handler.rb +453 -0
  41. data/lib/active_support/multibyte/handlers/utf8_handler_proc.rb +44 -0
  42. data/lib/active_support/option_merger.rb +3 -3
  43. data/lib/active_support/ordered_options.rb +24 -23
  44. data/lib/active_support/reloadable.rb +39 -5
  45. data/lib/active_support/values/time_zone.rb +1 -1
  46. data/lib/active_support/values/unicode_tables.dat +0 -0
  47. data/lib/active_support/vendor/builder/blankslate.rb +16 -6
  48. data/lib/active_support/vendor/builder/xchar.rb +112 -0
  49. data/lib/active_support/vendor/builder/xmlbase.rb +12 -10
  50. data/lib/active_support/vendor/builder/xmlmarkup.rb +26 -7
  51. data/lib/active_support/vendor/xml_simple.rb +1021 -0
  52. data/lib/active_support/version.rb +2 -2
  53. data/lib/active_support/whiny_nil.rb +1 -1
  54. metadata +26 -4
  55. 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