connection.rb 5.2 KB
Newer Older
1 2 3 4
require 'net/https'
require 'date'
require 'time'
require 'uri'
5
require 'benchmark'
6 7

module ActiveResource
8
  class ConnectionError < StandardError # :nodoc:
9 10 11 12 13 14
    attr_reader :response

    def initialize(response, message = nil)
      @response = response
      @message  = message
    end
15

16
    def to_s
17
      "Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
18 19
    end
  end
20

21 22 23 24 25
  # 3xx Redirection
  class Redirection < ConnectionError # :nodoc:
    def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end    
  end 

26 27 28
  # 4xx Client Error
  class ClientError < ConnectionError; end # :nodoc:
  
29 30 31 32 33 34 35 36 37
  # 400 Bad Request
  class BadRequest < ClientError; end # :nodoc
  
  # 401 Unauthorized
  class UnauthorizedAccess < ClientError; end # :nodoc
  
  # 403 Forbidden
  class ForbiddenAccess < ClientError; end # :nodoc
  
38 39 40 41 42
  # 404 Not Found
  class ResourceNotFound < ClientError; end # :nodoc:
  
  # 409 Conflict
  class ResourceConflict < ClientError; end # :nodoc:
43

44 45
  # 5xx Server Error
  class ServerError < ConnectionError; end # :nodoc:
46

47
  # 405 Method Not Allowed
48
  class MethodNotAllowed < ClientError # :nodoc:
49 50 51 52 53
    def allowed_methods
      @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
    end
  end

54 55 56
  # Class to handle connections to remote web services.
  # This class is used by ActiveResource::Base to interface with REST
  # services.
57
  class Connection
58
    attr_reader :site
59
    attr_accessor :format
60

61 62 63 64 65 66
    class << self
      def requests
        @@requests ||= []
      end
    end

67 68
    # The +site+ parameter is required and will set the +site+
    # attribute to the URI for the remote resource service.
69
    def initialize(site, format = ActiveResource::Formats[:xml])
70 71
      raise ArgumentError, 'Missing site URI' unless site
      self.site = site
72
      self.format = format
73
    end
74

75
    # Set URI for remote service.
76 77
    def site=(site)
      @site = site.is_a?(URI) ? site : URI.parse(site)
78
    end
79

80 81
    # Execute a GET request.
    # Used to get (find) resources.
82
    def get(path, headers = {})
83
      format.decode(request(:get, path, build_request_headers(headers)).body)
84
    end
85

86 87
    # Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
    # Used to delete resources.
88 89
    def delete(path, headers = {})
      request(:delete, path, build_request_headers(headers))
90
    end
91

92 93
    # Execute a PUT request (see HTTP protocol documentation if unfamiliar).
    # Used to update resources.
94
    def put(path, body = '', headers = {})
95
      request(:put, path, body.to_s, build_request_headers(headers))
96 97
    end

98 99
    # Execute a POST request.
    # Used to create new resources.
100
    def post(path, body = '', headers = {})
101
      request(:post, path, body.to_s, build_request_headers(headers))
102
    end
103

104

105
    private
106
      # Makes request to remote service.
107
      def request(method, path, *arguments)
108
        logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
109 110
        result = nil
        time = Benchmark.realtime { result = http.send(method, path, *arguments) }
111
        logger.info "--> #{result.code} #{result.message} (#{result.body ? result.body : 0}b %.2fs)" % time if logger
112
        handle_response(result)
113
      end
114

115
      # Handles response and error codes from remote service.
116
      def handle_response(response)
117
        case response.code.to_i
118 119
          when 301,302
            raise(Redirection.new(response))
120
          when 200...400
121
            response
122 123 124 125 126 127
          when 400
            raise(BadRequest.new(response))
          when 401
            raise(UnauthorizedAccess.new(response))
          when 403
            raise(ForbiddenAccess.new(response))
128 129
          when 404
            raise(ResourceNotFound.new(response))
130 131
          when 405
            raise(MethodNotAllowed.new(response))
132 133
          when 409
            raise(ResourceConflict.new(response))
134 135
          when 422
            raise(ResourceInvalid.new(response))
136
          when 401...500
137 138 139 140 141 142 143 144
            raise(ClientError.new(response))
          when 500...600
            raise(ServerError.new(response))
          else
            raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
        end
      end

145
      # Creates new Net::HTTP instance for communication with
146
      # remote service and resources.
147
      def http
148 149 150 151
        http             = Net::HTTP.new(@site.host, @site.port)
        http.use_ssl     = @site.is_a?(URI::HTTPS)
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
        http
152
      end
153 154 155 156

      def default_header
        @default_header ||= { 'Content-Type' => format.mime_type }
      end
157
      
158
      # Builds headers for request to remote service.
159
      def build_request_headers(headers)
160
        authorization_header.update(default_header).update(headers)
161 162
      end
      
163
      # Sets authorization header; authentication information is pulled from credentials provided with site URI.
164 165 166
      def authorization_header
        (@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
      end
167

168
      def logger #:nodoc:
169 170
        ActiveResource::Base.logger
      end
171
  end
172
end