lib/patron/response.rb



## -------------------------------------------------------------------
##
## Patron HTTP Client: Response class
## Copyright (c) 2008 The Hive http://www.thehive.com/
##
## Permission is hereby granted, free of charge, to any person obtaining a copy
## of this software and associated documentation files (the "Software"), to deal
## in the Software without restriction, including without limitation the rights
## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
## copies of the Software, and to permit persons to whom the Software is
## furnished to do so, subject to the following conditions:
##
## The above copyright notice and this permission notice shall be included in
## all copies or substantial portions of the Software.
##
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
## THE SOFTWARE.
##
## -------------------------------------------------------------------


module Patron

  # Represents the response from the HTTP server.
  class Response
    # @return [String] the original URL used to perform the request (contains the final URL after redirects)
    attr_reader :url

    # @return [Fixnum] the HTTP status code of the final response after all the redirects
    attr_reader :status

    # @return [String] the complete status line (code and message)
    attr_reader :status_line

    # @return [Fixnum] how many redirects were followed when fulfilling this request
    attr_reader :redirect_count

    # @return [String, nil] the response body, or nil if the response was written directly to a file
    attr_reader :body

    # @return [Hash] the response headers. If there were multiple headers received for the same value
    #   (like "Cookie"), the header values will be within an Array under the key for the header, in order.
    attr_reader :headers

    # @return [String] the recognized name of the charset for the response
    attr_reader :charset

    # Overridden so that the output is shorter and there is no response body printed
    def inspect
      # Avoid spamming the console with the header and body data
      "#<Patron::Response @status_line='#{@status_line}'>"
    end

    def initialize(url, status, redirect_count, header_data, body, default_charset = nil)
      # Don't let a response clear out the default charset, which would cause encoding to fail
      default_charset = "ASCII-8BIT" unless default_charset
      @url            = url
      @status         = status
      @redirect_count = redirect_count
      @body           = body

      @charset        = determine_charset(header_data, body) || default_charset

      [url, header_data].each do |attr|
        convert_to_default_encoding!(attr)
      end

      parse_headers(header_data)
      if @headers["Content-Type"] && @headers["Content-Type"][0, 5] == "text/"
        convert_to_default_encoding!(@body)
      end
    end

    # Tells whether the HTTP response code is less than 400
    #
    # @return [Boolean]
    def ok?
      !error?
    end
    
    # Tells whether the HTTP response code is larger than 399
    #
    # @return [Boolean]
    def error?
      status >= 400
    end
    
    private

    def determine_charset(header_data, body)
      header_data.match(charset_regex) || (body && body.match(charset_regex))

      charset = $1
      validate_charset(charset) ? charset : nil
    end

    def charset_regex
      /(?:charset|encoding)="?([a-z0-9-]+)"?/i
    end

    def validate_charset(charset)
      charset && Encoding.find(charset) && true
    rescue ArgumentError
      false
    end

    def convert_to_default_encoding!(str)
      if str.respond_to?(:encode) && Encoding.default_internal
        str.force_encoding(charset).encode!(Encoding.default_internal)
      end
    end

    # Called by the C code to parse and set the headers
    def parse_headers(header_data)
      @headers = {}

      lines = header_data.split("\r\n")

      @status_line = lines.shift

      lines.each do |line|
        break if line.empty?

        hdr, val = line.split(":", 2)

        val.strip! unless val.nil?

        if @headers.key?(hdr)
          @headers[hdr] = [@headers[hdr]] unless @headers[hdr].kind_of? Array
          @headers[hdr] << val
        else
          @headers[hdr] = val
        end
      end
    end

  end
end