diff options
Diffstat (limited to 'actionpack/lib/action_controller')
-rwxr-xr-x | actionpack/lib/action_controller/cgi_ext/cgi_methods.rb | 208 | ||||
-rw-r--r-- | actionpack/lib/action_controller/vendor/xml_simple.rb | 1019 |
2 files changed, 74 insertions, 1153 deletions
diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb index be5923e8de..1af837c39a 100755 --- a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb +++ b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb @@ -1,14 +1,13 @@ require 'cgi' -require 'action_controller/vendor/xml_simple' require 'action_controller/vendor/xml_node' # Static methods for parsing the query and request parameters that can be used in # a CGI extension class or testing in isolation. class CGIMethods #:nodoc: - public + class << self # Returns a hash with the pairs from the query string. The implicit hash construction that is done in # parse_request_params is not done here. - def CGIMethods.parse_query_parameters(query_string) + def parse_query_parameters(query_string) parsed_params = {} query_string.split(/[&;]/).each { |p| @@ -41,7 +40,7 @@ class CGIMethods #:nodoc: # Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" / # "Somewhere cool!" are translated into a full hash hierarchy, like # { "customer" => { "address" => { "street" => "Somewhere cool!" } } } - def CGIMethods.parse_request_parameters(params) + def parse_request_parameters(params) parsed_params = {} for key, value in params @@ -59,162 +58,103 @@ class CGIMethods #:nodoc: parsed_params end - def self.parse_formatted_request_parameters(mime_type, raw_post_data) - params = case strategy = ActionController::Base.param_parsers[mime_type] + def parse_formatted_request_parameters(mime_type, raw_post_data) + case strategy = ActionController::Base.param_parsers[mime_type] when Proc strategy.call(raw_post_data) when :xml_simple - raw_post_data.blank? ? nil : - typecast_xml_value(XmlSimple.xml_in(raw_post_data, - 'forcearray' => false, - 'forcecontent' => true, - 'keeproot' => true, - 'contentkey' => '__content__')) + raw_post_data.blank? ? {} : Hash.create_from_xml(raw_post_data) when :yaml YAML.load(raw_post_data) when :xml_node node = XmlNode.from_xml(raw_post_data) { node.node_name => node } end - - dasherize_keys(params || {}) rescue Object => e { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace, "raw_post_data" => raw_post_data, "format" => mime_type } end - def self.typecast_xml_value(value) - case value - when Hash - if value.has_key?("__content__") - content = translate_xml_entities(value["__content__"]) - case value["type"] - when "integer" then content.to_i - when "boolean" then content == "true" - when "datetime" then Time.parse(content) - when "date" then Date.parse(content) - else content - end - else - value.empty? ? nil : value.inject({}) do |h,(k,v)| - h[k] = typecast_xml_value(v) - h - end - end - when Array - value.map! { |i| typecast_xml_value(i) } - case value.length - when 0 then nil - when 1 then value.first - else value - end - else - raise "can't typecast #{value.inspect}" - end - end - - private - - def self.translate_xml_entities(value) - value.gsub(/</, "<"). - gsub(/>/, ">"). - gsub(/"/, '"'). - gsub(/'/, "'"). - gsub(/&/, "&") - end - - def self.dasherize_keys(params) - case params.class.to_s - when "Hash" - params.inject({}) do |h,(k,v)| - h[k.to_s.tr("-", "_")] = dasherize_keys(v) - h - end - when "Array" - params.map { |v| dasherize_keys(v) } - else - params - end - end - - # Splits the given key into several pieces. Example keys are 'name', 'person[name]', - # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned. - # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', ''] - def CGIMethods.split_key(key) - if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key - keys = [$1] + private + # Splits the given key into several pieces. Example keys are 'name', 'person[name]', + # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned. + # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', ''] + def split_key(key) + if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key + keys = [$1] - keys.concat($2[1..-2].split('][')) - keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings + keys.concat($2[1..-2].split('][')) + keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings - keys - else - [key] + keys + else + [key] + end end - end - def CGIMethods.get_typed_value(value) - # test most frequent case first - if value.is_a?(String) - value - elsif value.respond_to?(:content_type) && ! value.content_type.blank? - # Uploaded file - unless value.respond_to?(:full_original_filename) - class << value - alias_method :full_original_filename, :original_filename - - # Take the basename of the upload's original filename. - # This handles the full Windows paths given by Internet Explorer - # (and perhaps other broken user agents) without affecting - # those which give the lone filename. - # The Windows regexp is adapted from Perl's File::Basename. - def original_filename - if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename) - md.captures.first - else - File.basename full_original_filename + def get_typed_value(value) + # test most frequent case first + if value.is_a?(String) + value + elsif value.respond_to?(:content_type) && ! value.content_type.blank? + # Uploaded file + unless value.respond_to?(:full_original_filename) + class << value + alias_method :full_original_filename, :original_filename + + # Take the basename of the upload's original filename. + # This handles the full Windows paths given by Internet Explorer + # (and perhaps other broken user agents) without affecting + # those which give the lone filename. + # The Windows regexp is adapted from Perl's File::Basename. + def original_filename + if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename) + md.captures.first + else + File.basename full_original_filename + end end end end - end - # Return the same value after overriding original_filename. - value + # Return the same value after overriding original_filename. + value - elsif value.respond_to?(:read) - # Value as part of a multipart request - result = value.read - value.rewind - result - elsif value.class == Array - value.collect { |v| CGIMethods.get_typed_value(v) } - else - # other value (neither string nor a multipart request) - value.to_s + elsif value.respond_to?(:read) + # Value as part of a multipart request + result = value.read + value.rewind + result + elsif value.class == Array + value.collect { |v| get_typed_value(v) } + else + # other value (neither string nor a multipart request) + value.to_s + end end - end - PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/ - def CGIMethods.get_levels(key) - all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a - if main.nil? - [] - elsif trailing - [key] - elsif bracketed - [main] + bracketed.slice(1...-1).split('][') - else - [main] + PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/ + def get_levels(key) + all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a + if main.nil? + [] + elsif trailing + [key] + elsif bracketed + [main] + bracketed.slice(1...-1).split('][') + else + [main] + end end - end - def CGIMethods.build_deep_hash(value, hash, levels) - if levels.length == 0 - value - elsif hash.nil? - { levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) } - else - hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) }) + def build_deep_hash(value, hash, levels) + if levels.length == 0 + value + elsif hash.nil? + { levels.first => build_deep_hash(value, nil, levels[1..-1]) } + else + hash.update({ levels.first => build_deep_hash(value, hash[levels.first], levels[1..-1]) }) + end end - end + end end diff --git a/actionpack/lib/action_controller/vendor/xml_simple.rb b/actionpack/lib/action_controller/vendor/xml_simple.rb deleted file mode 100644 index e6ce63a839..0000000000 --- a/actionpack/lib/action_controller/vendor/xml_simple.rb +++ /dev/null @@ -1,1019 +0,0 @@ -# = XmlSimple -# -# Author:: Maik Schmidt <contact@maik-schmidt.de> -# Copyright:: Copyright (c) 2003 Maik Schmidt -# License:: Distributes under the same terms as Ruby. -# -require 'rexml/document' - -# Easy API to maintain XML (especially configuration files). -class XmlSimple #:nodoc: - include REXML - - @@VERSION = '1.0.2' - - # A simple cache for XML documents that were already transformed - # by xml_in. - class Cache #:nodoc: - # Creates and initializes a new Cache object. - def initialize - @mem_share_cache = {} - @mem_copy_cache = {} - end - - # Saves a data structure into a file. - # - # data:: - # Data structure to be saved. - # filename:: - # Name of the file belonging to the data structure. - def save_storable(data, filename) - cache_file = get_cache_filename(filename) - File.open(cache_file, "w+") { |f| Marshal.dump(data, f) } - end - - # Restores a data structure from a file. If restoring the data - # structure failed for any reason, nil will be returned. - # - # filename:: - # Name of the file belonging to the data structure. - def restore_storable(filename) - cache_file = get_cache_filename(filename) - return nil unless File::exist?(cache_file) - return nil unless File::mtime(cache_file).to_i > File::mtime(filename).to_i - data = nil - File.open(cache_file) { |f| data = Marshal.load(f) } - data - end - - # Saves a data structure in a shared memory cache. - # - # data:: - # Data structure to be saved. - # filename:: - # Name of the file belonging to the data structure. - def save_mem_share(data, filename) - @mem_share_cache[filename] = [Time::now.to_i, data] - end - - # Restores a data structure from a shared memory cache. You - # should consider these elements as "read only". If restoring - # the data structure failed for any reason, nil will be - # returned. - # - # filename:: - # Name of the file belonging to the data structure. - def restore_mem_share(filename) - get_from_memory_cache(filename, @mem_share_cache) - end - - # Copies a data structure to a memory cache. - # - # data:: - # Data structure to be copied. - # filename:: - # Name of the file belonging to the data structure. - def save_mem_copy(data, filename) - @mem_share_cache[filename] = [Time::now.to_i, Marshal.dump(data)] - end - - # Restores a data structure from a memory cache. If restoring - # the data structure failed for any reason, nil will be - # returned. - # - # filename:: - # Name of the file belonging to the data structure. - def restore_mem_copy(filename) - data = get_from_memory_cache(filename, @mem_share_cache) - data = Marshal.load(data) unless data.nil? - data - end - - private - - # Returns the "cache filename" belonging to a filename, i.e. - # the extension '.xml' in the original filename will be replaced - # by '.stor'. If filename does not have this extension, '.stor' - # will be appended. - # - # filename:: - # Filename to get "cache filename" for. - def get_cache_filename(filename) - filename.sub(/(\.xml)?$/, '.stor') - end - - # Returns a cache entry from a memory cache belonging to a - # certain filename. If no entry could be found for any reason, - # nil will be returned. - # - # filename:: - # Name of the file the cache entry belongs to. - # cache:: - # Memory cache to get entry from. - def get_from_memory_cache(filename, cache) - return nil unless cache[filename] - return nil unless cache[filename][0] > File::mtime(filename).to_i - return cache[filename][1] - end - end - - # Create a "global" cache. - @@cache = Cache.new - - # Creates and intializes a new XmlSimple object. - # - # defaults:: - # Default values for options. - def initialize(defaults = nil) - unless defaults.nil? || defaults.instance_of?(Hash) - raise ArgumentError, "Options have to be a Hash." - end - @default_options = normalize_option_names(defaults, KNOWN_OPTIONS['in'] & KNOWN_OPTIONS['out']) - @options = Hash.new - @_var_values = nil - end - - # Converts an XML document in the same way as the Perl module XML::Simple. - # - # string:: - # XML source. Could be one of the following: - # - # - nil: Tries to load and parse '<scriptname>.xml'. - # - filename: Tries to load and parse filename. - # - IO object: Reads from object until EOF is detected and parses result. - # - XML string: Parses string. - # - # options:: - # Options to be used. - def xml_in(string = nil, options = nil) - handle_options('in', options) - - # If no XML string or filename was supplied look for scriptname.xml. - if string.nil? - string = File::basename($0) - string.sub!(/\.[^.]+$/, '') - string += '.xml' - - directory = File::dirname($0) - @options['searchpath'].unshift(directory) unless directory.nil? - end - - if string.instance_of?(String) - if string =~ /<.*?>/m - @doc = parse(string) - elsif string == '-' - @doc = parse($stdin.readlines.to_s) - else - filename = find_xml_file(string, @options['searchpath']) - - if @options.has_key?('cache') - @options['cache'].each { |scheme| - case(scheme) - when 'storable' - content = @@cache.restore_storable(filename) - when 'mem_share' - content = @@cache.restore_mem_share(filename) - when 'mem_copy' - content = @@cache.restore_mem_copy(filename) - else - raise ArgumentError, "Unsupported caching scheme: <#{scheme}>." - end - return content if content - } - end - - @doc = load_xml_file(filename) - end - elsif string.kind_of?(IO) - @doc = parse(string.readlines.to_s) - else - raise ArgumentError, "Could not parse object of type: <#{string.type}>." - end - - result = collapse(@doc.root) - result = @options['keeproot'] ? merge({}, @doc.root.name, result) : result - put_into_cache(result, filename) - result - end - - # This is the functional version of the instance method xml_in. - def XmlSimple.xml_in(string = nil, options = nil) - xml_simple = XmlSimple.new - xml_simple.xml_in(string, options) - end - - # Converts a data structure into an XML document. - # - # ref:: - # Reference to data structure to be converted into XML. - # options:: - # Options to be used. - def xml_out(ref, options = nil) - handle_options('out', options) - if ref.instance_of?(Array) - ref = { @options['anonymoustag'] => ref } - end - - if @options['keeproot'] - keys = ref.keys - if keys.size == 1 - ref = ref[keys[0]] - @options['rootname'] = keys[0] - end - elsif @options['rootname'] == '' - if ref.instance_of?(Hash) - refsave = ref - ref = {} - refsave.each { |key, value| - if !scalar(value) - ref[key] = value - else - ref[key] = [ value.to_s ] - end - } - end - end - - @ancestors = [] - xml = value_to_xml(ref, @options['rootname'], '') - @ancestors = nil - - if @options['xmldeclaration'] - xml = @options['xmldeclaration'] + "\n" + xml - end - - if @options.has_key?('outputfile') - if @options['outputfile'].kind_of?(IO) - return @options['outputfile'].write(xml) - else - File.open(@options['outputfile'], "w") { |file| file.write(xml) } - end - end - xml - end - - # This is the functional version of the instance method xml_out. - def XmlSimple.xml_out(hash, options = nil) - xml_simple = XmlSimple.new - xml_simple.xml_out(hash, options) - end - - private - - # Declare options that are valid for xml_in and xml_out. - KNOWN_OPTIONS = { - 'in' => %w( - keyattr keeproot forcecontent contentkey noattr - searchpath forcearray suppressempty anonymoustag - cache grouptags normalisespace normalizespace - variables varattr - ), - 'out' => %w( - keyattr keeproot contentkey noattr rootname - xmldeclaration outputfile noescape suppressempty - anonymoustag indent grouptags noindent - ) - } - - # Define some reasonable defaults. - DEF_KEY_ATTRIBUTES = [] - DEF_ROOT_NAME = 'opt' - DEF_CONTENT_KEY = 'content' - DEF_XML_DECLARATION = "<?xml version='1.0' standalone='yes'?>" - DEF_ANONYMOUS_TAG = 'anon' - DEF_FORCE_ARRAY = true - DEF_INDENTATION = ' ' - - # Normalizes option names in a hash, i.e., turns all - # characters to lower case and removes all underscores. - # Additionally, this method checks, if an unknown option - # was used and raises an according exception. - # - # options:: - # Hash to be normalized. - # known_options:: - # List of known options. - def normalize_option_names(options, known_options) - return nil if options.nil? - result = Hash.new - options.each { |key, value| - lkey = key.downcase - lkey.gsub!(/_/, '') - if !known_options.member?(lkey) - raise ArgumentError, "Unrecognised option: #{lkey}." - end - result[lkey] = value - } - result - end - - # Merges a set of options with the default options. - # - # direction:: - # 'in': If options should be handled for xml_in. - # 'out': If options should be handled for xml_out. - # options:: - # Options to be merged with the default options. - def handle_options(direction, options) - @options = options || Hash.new - - raise ArgumentError, "Options must be a Hash!" unless @options.instance_of?(Hash) - - unless KNOWN_OPTIONS.has_key?(direction) - raise ArgumentError, "Unknown direction: <#{direction}>." - end - - known_options = KNOWN_OPTIONS[direction] - @options = normalize_option_names(@options, known_options) - - unless @default_options.nil? - known_options.each { |option| - unless @options.has_key?(option) - if @default_options.has_key?(option) - @options[option] = @default_options[option] - end - end - } - end - - unless @options.has_key?('noattr') - @options['noattr'] = false - end - - if @options.has_key?('rootname') - @options['rootname'] = '' if @options['rootname'].nil? - else - @options['rootname'] = DEF_ROOT_NAME - end - - if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true - @options['xmldeclaration'] = DEF_XML_DECLARATION - end - - if @options.has_key?('contentkey') - if @options['contentkey'] =~ /^-(.*)$/ - @options['contentkey'] = $1 - @options['collapseagain'] = true - end - else - @options['contentkey'] = DEF_CONTENT_KEY - end - - unless @options.has_key?('normalisespace') - @options['normalisespace'] = @options['normalizespace'] - end - @options['normalisespace'] = 0 if @options['normalisespace'].nil? - - if @options.has_key?('searchpath') - unless @options['searchpath'].instance_of?(Array) - @options['searchpath'] = [ @options['searchpath'] ] - end - else - @options['searchpath'] = [] - end - - if @options.has_key?('cache') && scalar(@options['cache']) - @options['cache'] = [ @options['cache'] ] - end - - @options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag') - - if !@options.has_key?('indent') || @options['indent'].nil? - @options['indent'] = DEF_INDENTATION - end - - @options['indent'] = '' if @options.has_key?('noindent') - - # Special cleanup for 'keyattr' which could be an array or - # a hash or left to default to array. - if @options.has_key?('keyattr') - if !scalar(@options['keyattr']) - # Convert keyattr => { elem => '+attr' } - # to keyattr => { elem => ['attr', '+'] } - if @options['keyattr'].instance_of?(Hash) - @options['keyattr'].each { |key, value| - if value =~ /^([-+])?(.*)$/ - @options['keyattr'][key] = [$2, $1 ? $1 : ''] - end - } - elsif !@options['keyattr'].instance_of?(Array) - raise ArgumentError, "'keyattr' must be String, Hash, or Array!" - end - else - @options['keyattr'] = [ @options['keyattr'] ] - end - else - @options['keyattr'] = DEF_KEY_ATTRIBUTES - end - - if @options.has_key?('forcearray') - if @options['forcearray'].instance_of?(Regexp) - @options['forcearray'] = [ @options['forcearray'] ] - end - - if @options['forcearray'].instance_of?(Array) - force_list = @options['forcearray'] - unless force_list.empty? - @options['forcearray'] = {} - force_list.each { |tag| - if tag.instance_of?(Regexp) - unless @options['forcearray']['_regex'].instance_of?(Array) - @options['forcearray']['_regex'] = [] - end - @options['forcearray']['_regex'] << tag - else - @options['forcearray'][tag] = true - end - } - else - @options['forcearray'] = false - end - else - @options['forcearray'] = @options['forcearray'] ? true : false - end - else - @options['forcearray'] = DEF_FORCE_ARRAY - end - - if @options.has_key?('grouptags') && !@options['grouptags'].instance_of?(Hash) - raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash." - end - - if @options.has_key?('variables') && !@options['variables'].instance_of?(Hash) - raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash." - end - - if @options.has_key?('variables') - @_var_values = @options['variables'] - elsif @options.has_key?('varattr') - @_var_values = {} - end - end - - # Actually converts an XML document element into a data structure. - # - # element:: - # The document element to be collapsed. - def collapse(element) - result = @options['noattr'] ? {} : get_attributes(element) - - if @options['normalisespace'] == 2 - result.each { |k, v| result[k] = normalise_space(v) } - end - - if element.has_elements? - element.each_element { |child| - value = collapse(child) - if empty(value) && (element.attributes.empty? || @options['noattr']) - next if @options.has_key?('suppressempty') && @options['suppressempty'] == true - end - result = merge(result, child.name, value) - } - if has_mixed_content?(element) - # normalisespace? - content = element.texts.map { |x| x.to_s } - content = content[0] if content.size == 1 - result[@options['contentkey']] = content - end - elsif element.has_text? # i.e. it has only text. - return collapse_text_node(result, element) - end - - # Turn Arrays into Hashes if key fields present. - count = fold_arrays(result) - - # Disintermediate grouped tags. - if @options.has_key?('grouptags') - result.each { |key, value| - next unless (value.instance_of?(Hash) && (value.size == 1)) - child_key, child_value = value.to_a[0] - if @options['grouptags'][key] == child_key - result[key] = child_value - end - } - end - - # Fold Hases containing a single anonymous Array up into just the Array. - if count == 1 - anonymoustag = @options['anonymoustag'] - if result.has_key?(anonymoustag) && result[anonymoustag].instance_of?(Array) - return result[anonymoustag] - end - end - - if result.empty? && @options.has_key?('suppressempty') - return @options['suppressempty'] == '' ? '' : nil - end - - result - end - - # Collapses a text node and merges it with an existing Hash, if - # possible. - # Thanks to Curtis Schofield for reporting a subtle bug. - # - # hash:: - # Hash to merge text node value with, if possible. - # element:: - # Text node to be collapsed. - def collapse_text_node(hash, element) - value = node_to_text(element) - if empty(value) && !element.has_attributes? - return {} - end - - if element.has_attributes? && !@options['noattr'] - return merge(hash, @options['contentkey'], value) - else - if @options['forcecontent'] - return merge(hash, @options['contentkey'], value) - else - return value - end - end - end - - # Folds all arrays in a Hash. - # - # hash:: - # Hash to be folded. - def fold_arrays(hash) - fold_amount = 0 - keyattr = @options['keyattr'] - if (keyattr.instance_of?(Array) || keyattr.instance_of?(Hash)) - hash.each { |key, value| - if value.instance_of?(Array) - if keyattr.instance_of?(Array) - hash[key] = fold_array(value) - else - hash[key] = fold_array_by_name(key, value) - end - fold_amount += 1 - end - } - end - fold_amount - end - - # Folds an Array to a Hash, if possible. Folding happens - # according to the content of keyattr, which has to be - # an array. - # - # array:: - # Array to be folded. - def fold_array(array) - hash = Hash.new - array.each { |x| - return array unless x.instance_of?(Hash) - key_matched = false - @options['keyattr'].each { |key| - if x.has_key?(key) - key_matched = true - value = x[key] - return array if value.instance_of?(Hash) || value.instance_of?(Array) - value = normalise_space(value) if @options['normalisespace'] == 1 - x.delete(key) - hash[value] = x - break - end - } - return array unless key_matched - } - hash = collapse_content(hash) if @options['collapseagain'] - hash - end - - # Folds an Array to a Hash, if possible. Folding happens - # according to the content of keyattr, which has to be - # a Hash. - # - # name:: - # Name of the attribute to be folded upon. - # array:: - # Array to be folded. - def fold_array_by_name(name, array) - return array unless @options['keyattr'].has_key?(name) - key, flag = @options['keyattr'][name] - - hash = Hash.new - array.each { |x| - if x.instance_of?(Hash) && x.has_key?(key) - value = x[key] - return array if value.instance_of?(Hash) || value.instance_of?(Array) - value = normalise_space(value) if @options['normalisespace'] == 1 - hash[value] = x - hash[value]["-#{key}"] = hash[value][key] if flag == '-' - hash[value].delete(key) unless flag == '+' - else - $stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.") - return array - end - } - hash = collapse_content(hash) if @options['collapseagain'] - hash - end - - # Tries to collapse a Hash even more ;-) - # - # hash:: - # Hash to be collapsed again. - def collapse_content(hash) - content_key = @options['contentkey'] - hash.each_value { |value| - return hash unless value.instance_of?(Hash) && value.size == 1 && value.has_key?(content_key) - hash.each_key { |key| hash[key] = hash[key][content_key] } - } - hash - end - - # Adds a new key/value pair to an existing Hash. If the key to be added - # does already exist and the existing value associated with key is not - # an Array, it will be converted into an Array. Then the new value is - # appended to that Array. - # - # hash:: - # Hash to add key/value pair to. - # key:: - # Key to be added. - # value:: - # Value to be associated with key. - def merge(hash, key, value) - if value.instance_of?(String) - value = normalise_space(value) if @options['normalisespace'] == 2 - - # do variable substitutions - unless @_var_values.nil? || @_var_values.empty? - value.gsub!(/\$\{(\w+)\}/) { |x| get_var($1) } - end - - # look for variable definitions - if @options.has_key?('varattr') - varattr = @options['varattr'] - if hash.has_key?(varattr) - set_var(hash[varattr], value) - end - end - end - if hash.has_key?(key) - if hash[key].instance_of?(Array) - hash[key] << value - else - hash[key] = [ hash[key], value ] - end - elsif value.instance_of?(Array) # Handle anonymous arrays. - hash[key] = [ value ] - else - if force_array?(key) - hash[key] = [ value ] - else - hash[key] = value - end - end - hash - end - - # Checks, if the 'forcearray' option has to be used for - # a certain key. - def force_array?(key) - return false if key == @options['contentkey'] - return true if @options['forcearray'] == true - forcearray = @options['forcearray'] - if forcearray.instance_of?(Hash) - return true if forcearray.has_key?(key) - return false unless forcearray.has_key?('_regex') - forcearray['_regex'].each { |x| return true if key =~ x } - end - return false - end - - # Converts the attributes array of a document node into a Hash. - # Returns an empty Hash, if node has no attributes. - # - # node:: - # Document node to extract attributes from. - def get_attributes(node) - attributes = {} - node.attributes.each { |n,v| attributes[n] = v } - attributes - end - - # Determines, if a document element has mixed content. - # - # element:: - # Document element to be checked. - def has_mixed_content?(element) - if element.has_text? && element.has_elements? - return true if element.texts.join('') !~ /^\s*$/s - end - false - end - - # Called when a variable definition is encountered in the XML. - # A variable definition looks like - # <element attrname="name">value</element> - # where attrname matches the varattr setting. - def set_var(name, value) - @_var_values[name] = value - end - - # Called during variable substitution to get the value for the - # named variable. - def get_var(name) - if @_var_values.has_key?(name) - return @_var_values[name] - else - return "${#{name}}" - end - end - - # Recurses through a data structure building up and returning an - # XML representation of that structure as a string. - # - # ref:: - # Reference to the data structure to be encoded. - # name:: - # The XML tag name to be used for this item. - # indent:: - # A string of spaces for use as the current indent level. - def value_to_xml(ref, name, indent) - named = !name.nil? && name != '' - nl = @options.has_key?('noindent') ? '' : "\n" - - if !scalar(ref) - if @ancestors.member?(ref) - raise ArgumentError, "Circular data structures not supported!" - end - @ancestors << ref - else - if named - return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '</', name, '>', nl].join('') - else - return ref.to_s + nl - end - end - - # Unfold hash to array if possible. - if ref.instance_of?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != '' - ref = hash_to_array(name, ref) - end - - result = [] - if ref.instance_of?(Hash) - # Reintermediate grouped values if applicable. - if @options.has_key?('grouptags') - ref.each { |key, value| - if @options['grouptags'].has_key?(key) - ref[key] = { @options['grouptags'][key] => value } - end - } - end - - nested = [] - text_content = nil - if named - result << indent << '<' << name - end - - if !ref.empty? - ref.each { |key, value| - next if !key.nil? && key[0, 1] == '-' - if value.nil? - unless @options.has_key?('suppressempty') && @options['suppressempty'].nil? - raise ArgumentError, "Use of uninitialized value!" - end - value = {} - end - - if !scalar(value) || @options['noattr'] - nested << value_to_xml(value, key, indent + @options['indent']) - else - value = value.to_s - value = escape_value(value) unless @options['noescape'] - if key == @options['contentkey'] - text_content = value - else - result << ' ' << key << '="' << value << '"' - end - end - } - else - text_content = '' - end - - if !nested.empty? || !text_content.nil? - if named - result << '>' - if !text_content.nil? - result << text_content - nested[0].sub!(/^\s+/, '') if !nested.empty? - else - result << nl - end - if !nested.empty? - result << nested << indent - end - result << '</' << name << '>' << nl - else - result << nested - end - else - result << ' />' << nl - end - elsif ref.instance_of?(Array) - ref.each { |value| - if scalar(value) - result << indent << '<' << name << '>' - result << (@options['noescape'] ? value.to_s : escape_value(value.to_s)) - result << '</' << name << '>' << nl - elsif value.instance_of?(Hash) - result << value_to_xml(value, name, indent) - else - result << indent << '<' << name << '>' << nl - result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent']) - result << indent << '</' << name << '>' << nl - end - } - else - # Probably, this is obsolete. - raise ArgumentError, "Can't encode a value of type: #{ref.type}." - end - @ancestors.pop if !scalar(ref) - result.join('') - end - - # Checks, if a certain value is a "scalar" value. Whatever - # that will be in Ruby ... ;-) - # - # value:: - # Value to be checked. - def scalar(value) - return false if value.instance_of?(Hash) || value.instance_of?(Array) - return true - end - - # Attempts to unfold a hash of hashes into an array of hashes. Returns - # a reference to th array on success or the original hash, if unfolding - # is not possible. - # - # parent:: - # - # hashref:: - # Reference to the hash to be unfolded. - def hash_to_array(parent, hashref) - arrayref = [] - hashref.each { |key, value| - return hashref unless value.instance_of?(Hash) - - if @options['keyattr'].instance_of?(Hash) - return hashref unless @options['keyattr'].has_key?(parent) - arrayref << { @options['keyattr'][parent][0] => key }.update(value) - else - arrayref << { @options['keyattr'][0] => key }.update(value) - end - } - arrayref - end - - # Replaces XML markup characters by their external entities. - # - # data:: - # The string to be escaped. - def escape_value(data) - return data if data.nil? || data == '' - result = data.dup - result.gsub!('&', '&') - result.gsub!('<', '<') - result.gsub!('>', '>') - result.gsub!('"', '"') - result.gsub!("'", ''') - result - end - - # Removes leading and trailing whitespace and sequences of - # whitespaces from a string. - # - # text:: - # String to be normalised. - def normalise_space(text) - text.sub!(/^\s+/, '') - text.sub!(/\s+$/, '') - text.gsub!(/\s\s+/, ' ') - text - end - - # Checks, if an object is nil, an empty String or an empty Hash. - # Thanks to Norbert Gawor for a bugfix. - # - # value:: - # Value to be checked for emptyness. - def empty(value) - case value - when Hash - return value.empty? - when String - return value !~ /\S/m - else - return value.nil? - end - end - - # Converts a document node into a String. - # If the node could not be converted into a String - # for any reason, default will be returned. - # - # node:: - # Document node to be converted. - # default:: - # Value to be returned, if node could not be converted. - def node_to_text(node, default = nil) - if node.instance_of?(Element) - return node.texts.join('') - elsif node.instance_of?(Attribute) - return node.value.nil? ? default : node.value.strip - elsif node.instance_of?(Text) - return node.to_s.strip - else - return default - end - end - - # Parses an XML string and returns the according document. - # - # xml_string:: - # XML string to be parsed. - # - # The following exception may be raised: - # - # REXML::ParseException:: - # If the specified file is not wellformed. - def parse(xml_string) - Document.new(xml_string) - end - - # Searches in a list of paths for a certain file. Returns - # the full path to the file, if it could be found. Otherwise, - # an exception will be raised. - # - # filename:: - # Name of the file to search for. - # searchpath:: - # List of paths to search in. - def find_xml_file(file, searchpath) - filename = File::basename(file) - - if filename != file - return file if File::file?(file) - else - searchpath.each { |path| - full_path = File::join(path, filename) - return full_path if File::file?(full_path) - } - end - - if searchpath.empty? - return file if File::file?(file) - raise ArgumentError, "File does not exist: #{file}." - end - raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>" - end - - # Loads and parses an XML configuration file. - # - # filename:: - # Name of the configuration file to be loaded. - # - # The following exceptions may be raised: - # - # Errno::ENOENT:: - # If the specified file does not exist. - # REXML::ParseException:: - # If the specified file is not wellformed. - def load_xml_file(filename) - parse(File.readlines(filename).to_s) - end - - # Caches the data belonging to a certain file. - # - # data:: - # Data to be cached. - # filename:: - # Name of file the data was read from. - def put_into_cache(data, filename) - if @options.has_key?('cache') - @options['cache'].each { |scheme| - case(scheme) - when 'storable' - @@cache.save_storable(data, filename) - when 'mem_share' - @@cache.save_mem_share(data, filename) - when 'mem_copy' - @@cache.save_mem_copy(data, filename) - else - raise ArgumentError, "Unsupported caching scheme: <#{scheme}>." - end - } - end - end -end - -# vim:sw=2 |