Merge pull request #20 from simplymeasured/feature/make-autorefresh-of-token-optional
[arvados.git] / lib / google / api_client.rb
index da83f5e2bf17e0800ba05e8a116701fbe15d40b6..4c9ee03c7a9a7b1657f5f0418f884512b5f3326a 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-require 'httpadapter'
-require 'json'
+
+require 'faraday'
+require 'faraday/utils'
+require 'multi_json'
+require 'compat/multi_json'
 require 'stringio'
 
+require 'google/api_client/version'
+require 'google/api_client/logging'
+require 'google/api_client/errors'
+require 'google/api_client/environment'
 require 'google/api_client/discovery'
+require 'google/api_client/request'
+require 'google/api_client/reference'
+require 'google/api_client/result'
+require 'google/api_client/media'
+require 'google/api_client/service_account'
+require 'google/api_client/batch'
+require 'google/api_client/railtie' if defined?(Rails)
 
 module Google
-  # TODO(bobaman): Document all this stuff.
 
   ##
-  # This class manages communication with a single API.
+  # This class manages APIs communication.
   class APIClient
+    include Google::APIClient::Logging
+    
     ##
-    # An error which is raised when there is an unexpected response or other
-    # transport error that prevents an operation from succeeding.
-    class TransmissionError < StandardError
-    end
-
+    # Creates a new Google API client.
+    #
+    # @param [Hash] options The configuration parameters for the client.
+    # @option options [Symbol, #generate_authenticated_request] :authorization
+    #   (:oauth_1)
+    #   The authorization mechanism used by the client.  The following
+    #   mechanisms are supported out-of-the-box:
+    #   <ul>
+    #     <li><code>:two_legged_oauth_1</code></li>
+    #     <li><code>:oauth_1</code></li>
+    #     <li><code>:oauth_2</code></li>
+    #   </ul>
+    # @option options [Boolean] :auto_refresh_token (true)
+    #   The setting that controls whether or not the api client attempts to
+    #   refresh authorization when a 401 is hit in #execute. If the token does 
+    #   not support it, this option is ignored.
+    # @option options [String] :application_name
+    #   The name of the application using the client.
+    # @option options [String] :application_version
+    #   The version number of the application using the client.
+    # @option options [String] :user_agent
+    #   ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
+    #   The user agent used by the client.  Most developers will want to
+    #   leave this value alone and use the `:application_name` option instead.
+    # @option options [String] :host ("www.googleapis.com")
+    #   The API hostname used by the client. This rarely needs to be changed.
+    # @option options [String] :port (443)
+    #   The port number used by the client. This rarely needs to be changed.
+    # @option options [String] :discovery_path ("/discovery/v1")
+    #   The discovery base path. This rarely needs to be changed.
     def initialize(options={})
-      @options = {
-        # TODO: What configuration options need to go here?
-      }.merge(options)
-      # Force immediate type-checking and short-cut resolution
-      self.parser
-      self.authorization
-      self.http_adapter
-      return self
-    end
+      logger.debug { "#{self.class} - Initializing client with options #{options}" }
+      
+      # Normalize key to String to allow indifferent access.
+      options = options.inject({}) do |accu, (key, value)|
+        accu[key.to_sym] = value
+        accu
+      end
+      # Almost all API usage will have a host of 'www.googleapis.com'.
+      self.host = options[:host] || 'www.googleapis.com'
+      self.port = options[:port] || 443
+      self.discovery_path = options[:discovery_path] || '/discovery/v1'
 
-    ##
-    # Returns the parser used by the client.
-    def parser
-      unless @options[:parser]
-        require 'google/api_client/parsers/json_parser'
-        # NOTE: Do not rely on this default value, as it may change
-        @options[:parser] = JSONParser
+      # Most developers will want to leave this value alone and use the
+      # application_name option.
+      if options[:application_name]
+        app_name = options[:application_name]
+        app_version = options[:application_version]
+        application_string = "#{app_name}/#{app_version || '0.0.0'}"
+      else
+        logger.warn { "#{self.class} - Please provide :application_name and :application_version when initializing the client" }
       end
-      return @options[:parser]
+      self.user_agent = options[:user_agent] || (
+        "#{application_string} " +
+        "google-api-ruby-client/#{Google::APIClient::VERSION::STRING} " +
+         ENV::OS_VERSION
+      ).strip
+      # The writer method understands a few Symbols and will generate useful
+      # default authentication mechanisms.
+      self.authorization =
+        options.key?(:authorization) ? options[:authorization] : :oauth_2
+      self.auto_refresh_token = options.fetch(:auto_refresh_token) { true }
+      self.key = options[:key]
+      self.user_ip = options[:user_ip]
+      @discovery_uris = {}
+      @discovery_documents = {}
+      @discovered_apis = {}
+            
+      return self
     end
 
     ##
     # Returns the authorization mechanism used by the client.
     #
     # @return [#generate_authenticated_request] The authorization mechanism.
-    def authorization
-      case @options[:authorization]
+    attr_reader :authorization
+
+    ##
+    # Sets the authorization mechanism used by the client.
+    #
+    # @param [#generate_authenticated_request] new_authorization
+    #   The new authorization mechanism.
+    def authorization=(new_authorization)
+      case new_authorization
       when :oauth_1, :oauth
         require 'signet/oauth_1/client'
         # NOTE: Do not rely on this default value, as it may change
-        @options[:authorization] = Signet::OAuth1::Client.new(
+        new_authorization = Signet::OAuth1::Client.new(
           :temporary_credential_uri =>
             'https://www.google.com/accounts/OAuthGetRequestToken',
           :authorization_uri =>
@@ -71,305 +137,496 @@ module Google
           :client_credential_key => 'anonymous',
           :client_credential_secret => 'anonymous'
         )
+      when :two_legged_oauth_1, :two_legged_oauth
+        require 'signet/oauth_1/client'
+        # NOTE: Do not rely on this default value, as it may change
+        new_authorization = Signet::OAuth1::Client.new(
+          :client_credential_key => nil,
+          :client_credential_secret => nil,
+          :two_legged => true
+        )
+      when :oauth_2
+        require 'signet/oauth_2/client'
+        # NOTE: Do not rely on this default value, as it may change
+        new_authorization = Signet::OAuth2::Client.new(
+          :authorization_uri =>
+            'https://accounts.google.com/o/oauth2/auth',
+          :token_credential_uri =>
+            'https://accounts.google.com/o/oauth2/token'
+        )
       when nil
         # No authorization mechanism
       else
-        if !@options[:authorization].respond_to?(
-            :generate_authenticated_request)
+        if !new_authorization.respond_to?(:generate_authenticated_request)
           raise TypeError,
             'Expected authorization mechanism to respond to ' +
             '#generate_authenticated_request.'
         end
       end
-      return @options[:authorization]
+      @authorization = new_authorization
+      return @authorization
     end
 
     ##
-    # Sets the authorization mechanism used by the client.
+    # The setting that controls whether or not the api client attempts to
+    # refresh authorization when a 401 is hit in #execute. 
     #
-    # @param [#generate_authenticated_request] new_authorization
-    #   The new authorization mechanism.
-    def authorization=(new_authorization)
-      @options[:authorization] = new_authorization
-      return self.authorization
+    # @return [Boolean]
+    attr_accessor :auto_refresh_token
+
+    ##
+    # The application's API key issued by the API console.
+    #
+    # @return [String] The API key.
+    attr_accessor :key
+
+    ##
+    # The IP address of the user this request is being performed on behalf of.
+    #
+    # @return [String] The user's IP address.
+    attr_accessor :user_ip
+
+    ##
+    # The user agent used by the client.
+    #
+    # @return [String]
+    #   The user agent string used in the User-Agent header.
+    attr_accessor :user_agent
+
+    ##
+    # The API hostname used by the client.
+    #
+    # @return [String]
+    #   The API hostname. Should almost always be 'www.googleapis.com'.
+    attr_accessor :host
+
+    ##
+    # The port number used by the client.
+    #
+    # @return [String]
+    #   The port number. Should almost always be 443.
+    attr_accessor :port
+
+    ##
+    # The base path used by the client for discovery.
+    #
+    # @return [String]
+    #   The base path. Should almost always be '/discovery/v1'.
+    attr_accessor :discovery_path
+
+    ##
+    # Returns the URI for the directory document.
+    #
+    # @return [Addressable::URI] The URI of the directory document.
+    def directory_uri
+      return resolve_uri(self.discovery_path + '/apis')
     end
 
     ##
-    # Returns the HTTP adapter used by the client.
-    def http_adapter
-      return @options[:http_adapter] ||= (begin
-        require 'httpadapter/adapters/net_http'
-        @options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter
-      end)
+    # Manually registers a URI as a discovery document for a specific version
+    # of an API.
+    #
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
+    # @param [Addressable::URI] uri The URI of the discovery document.
+    def register_discovery_uri(api, version, uri)
+      api = api.to_s
+      version = version || 'v1'
+      @discovery_uris["#{api}:#{version}"] = uri
     end
 
     ##
     # Returns the URI for the discovery document.
     #
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
     # @return [Addressable::URI] The URI of the discovery document.
-    def discovery_uri
-      return @options[:discovery_uri] ||= (begin
-        if @options[:service]
-          service_id = @options[:service]
-          service_version = @options[:service_version] || 'v1'
-          Addressable::URI.parse(
-            "http://www.googleapis.com/discovery/0.1/describe" +
-            "?api=#{service_id}"
-          )
-        else
-          raise ArgumentError,
-            'Missing required configuration value, :discovery_uri.'
-        end
+    def discovery_uri(api, version=nil)
+      api = api.to_s
+      version = version || 'v1'
+      return @discovery_uris["#{api}:#{version}"] ||= (
+        resolve_uri(
+          self.discovery_path + '/apis/{api}/{version}/rest',
+          'api' => api,
+          'version' => version
+        )
+      )
+    end
+
+    ##
+    # Manually registers a pre-loaded discovery document for a specific version
+    # of an API.
+    #
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
+    # @param [String, StringIO] discovery_document
+    #   The contents of the discovery document.
+    def register_discovery_document(api, version, discovery_document)
+      api = api.to_s
+      version = version || 'v1'
+      if discovery_document.kind_of?(StringIO)
+        discovery_document.rewind
+        discovery_document = discovery_document.string
+      elsif discovery_document.respond_to?(:to_str)
+        discovery_document = discovery_document.to_str
+      else
+        raise TypeError,
+          "Expected String or StringIO, got #{discovery_document.class}."
+      end
+      @discovery_documents["#{api}:#{version}"] =
+        MultiJson.load(discovery_document)
+    end
+
+    ##
+    # Returns the parsed directory document.
+    #
+    # @return [Hash] The parsed JSON from the directory document.
+    def directory_document
+      return @directory_document ||= (begin
+        response = self.execute!(
+          :http_method => :get,
+          :uri => self.directory_uri,
+          :authenticated => false
+        )
+        response.data
       end)
     end
 
     ##
     # Returns the parsed discovery document.
     #
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
     # @return [Hash] The parsed JSON from the discovery document.
-    def discovery_document
-      return @discovery_document ||= (begin
-        request = ['GET', self.discovery_uri.to_s, [], []]
-        response = self.transmit_request(request)
-        status, headers, body = response
-        if status == 200 # TODO(bobaman) Better status code handling?
-          merged_body = StringIO.new
-          body.each do |chunk|
-            merged_body.write(chunk)
-          end
-          merged_body.rewind
-          JSON.parse(merged_body.string)
-        else
-          raise TransmissionError,
-            "Could not retrieve discovery document at: #{self.discovery_uri}"
-        end
+    def discovery_document(api, version=nil)
+      api = api.to_s
+      version = version || 'v1'
+      return @discovery_documents["#{api}:#{version}"] ||= (begin
+        response = self.execute!(
+          :http_method => :get,
+          :uri => self.discovery_uri(api, version),
+          :authenticated => false
+        )
+        response.data
       end)
     end
 
     ##
-    # Returns a list of services this client instance has performed discovery
-    # for.  This may return multiple versions of the same service.
-    #
-    # @return [Array]
-    #   A list of discovered <code>Google::APIClient::Service</code> objects.
-    def discovered_services
-      return @discovered_services ||= (begin
-        service_names = self.discovery_document['data'].keys()
-        services = []
-        for service_name in service_names
-          versions = self.discovery_document['data'][service_name]
-          for service_version in versions.keys()
-            service_description =
-              self.discovery_document['data'][service_name][service_version]
-            services << ::Google::APIClient::Service.new(
-              service_name,
-              service_version,
-              service_description
+    # Returns all APIs published in the directory document.
+    #
+    # @return [Array] The list of available APIs.
+    def discovered_apis
+      @directory_apis ||= (begin
+        document_base = self.directory_uri
+        if self.directory_document && self.directory_document['items']
+          self.directory_document['items'].map do |discovery_document|
+            Google::APIClient::API.new(
+              document_base,
+              discovery_document
             )
           end
+        else
+          []
         end
-        services
       end)
     end
 
     ##
     # Returns the service object for a given service name and service version.
     #
-    # @param [String, Symbol] service_name The service name.
-    # @param [String] service_version The desired version of the service.
+    # @param [String, Symbol] api The API name.
+    # @param [String] version The desired version of the API.
     #
-    # @return [Google::APIClient::Service] The service object.
-    def discovered_service(service_name, service_version='v1')
-      if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
+    # @return [Google::APIClient::API] The service object.
+    def discovered_api(api, version=nil)
+      if !api.kind_of?(String) && !api.kind_of?(Symbol)
         raise TypeError,
-          "Expected String or Symbol, got #{service_name.class}."
+          "Expected String or Symbol, got #{api.class}."
       end
-      service_name = service_name.to_s
-      for service in self.discovered_services
-        if service.name == service_name &&
-            service.version.to_s == service_version.to_s
-          return service
+      api = api.to_s
+      version = version || 'v1'
+      return @discovered_apis["#{api}:#{version}"] ||= begin
+        document_base = self.discovery_uri(api, version)
+        discovery_document = self.discovery_document(api, version)
+        if document_base && discovery_document
+          Google::APIClient::API.new(
+            document_base,
+            discovery_document
+          )
+        else
+          nil
         end
       end
-      return nil
     end
 
     ##
     # Returns the method object for a given RPC name and service version.
     #
     # @param [String, Symbol] rpc_name The RPC name of the desired method.
-    # @param [String] service_version The desired version of the service.
+    # @param [String, Symbol] api The API the method is within.
+    # @param [String] version The desired version of the API.
     #
     # @return [Google::APIClient::Method] The method object.
-    def discovered_method(rpc_name, service_version='v1')
+    def discovered_method(rpc_name, api, version=nil)
       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
         raise TypeError,
           "Expected String or Symbol, got #{rpc_name.class}."
       end
       rpc_name = rpc_name.to_s
-      for service in self.discovered_services
-        # This looks kinda weird, but is not a real problem because there's
-        # almost always only one service, and this is memoized anyhow.
-        if service.version.to_s == service_version.to_s
-          return service.to_h[rpc_name] if service.to_h[rpc_name]
-        end
+      api = api.to_s
+      version = version || 'v1'
+      service = self.discovered_api(api, version)
+      if service.to_h[rpc_name]
+        return service.to_h[rpc_name]
+      else
+        return nil
       end
-      return nil
     end
 
     ##
     # Returns the service object with the highest version number.
     #
-    # <em>Warning</em>: This method should be used with great care. As APIs
-    # are updated, minor differences between versions may cause
+    # @note <em>Warning</em>: This method should be used with great care.
+    # As APIs are updated, minor differences between versions may cause
     # incompatibilities. Requesting a specific version will avoid this issue.
     #
-    # @param [String, Symbol] service_name The name of the service.
+    # @param [String, Symbol] api The name of the service.
     #
-    # @return [Google::APIClient::Service] The service object.
-    def latest_service_version(service_name)
-      if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
+    # @return [Google::APIClient::API] The service object.
+    def preferred_version(api)
+      if !api.kind_of?(String) && !api.kind_of?(Symbol)
         raise TypeError,
-          "Expected String or Symbol, got #{service_name.class}."
+          "Expected String or Symbol, got #{api.class}."
+      end
+      api = api.to_s
+      return self.discovered_apis.detect do |a|
+        a.name == api && a.preferred == true
+      end
+    end
+
+    ##
+    # Verifies an ID token against a server certificate. Used to ensure that
+    # an ID token supplied by an untrusted client-side mechanism is valid.
+    # Raises an error if the token is invalid or missing.
+    def verify_id_token!
+      require 'jwt'
+      require 'openssl'
+      @certificates ||= {}
+      if !self.authorization.respond_to?(:id_token)
+        raise ArgumentError, (
+          "Current authorization mechanism does not support ID tokens: " +
+          "#{self.authorization.class.to_s}"
+        )
+      elsif !self.authorization.id_token
+        raise ArgumentError, (
+          "Could not verify ID token, ID token missing. " +
+          "Scopes were: #{self.authorization.scope.inspect}"
+        )
+      else
+        check_cached_certs = lambda do
+          valid = false
+          for key, cert in @certificates
+            begin
+              self.authorization.decoded_id_token(cert.public_key)
+              valid = true
+            rescue JWT::DecodeError, Signet::UnsafeOperationError
+              # Expected exception. Ignore, ID token has not been validated.
+            end
+          end
+          valid
+        end
+        if check_cached_certs.call()
+          return true
+        end
+        response = self.execute!(
+          :http_method => :get,
+          :uri => 'https://www.googleapis.com/oauth2/v1/certs',
+          :authenticated => false
+        )
+        @certificates.merge!(
+          Hash[MultiJson.load(response.body).map do |key, cert|
+            [key, OpenSSL::X509::Certificate.new(cert)]
+          end]
+        )
+        if check_cached_certs.call()
+          return true
+        else
+          raise InvalidIDTokenError,
+            "Could not verify ID token against any available certificate."
+        end
       end
-      service_name = service_name.to_s
-      return (self.discovered_services.select do |service|
-        service.name == service_name
-      end).sort.last
+      return nil
     end
 
     ##
     # Generates a request.
     #
-    # @param [Google::APIClient::Method, String] api_method
+    # @option options [Google::APIClient::Method] :api_method
     #   The method object or the RPC name of the method being executed.
-    # @param [Hash, Array] parameters
+    # @option options [Hash, Array] :parameters
     #   The parameters to send to the method.
-    # @param [String] body The body of the request.
-    # @param [Hash, Array] headers The HTTP headers for the request.
-    # @param [Hash] options
-    #   The configuration parameters for the request.
-    #   - <code>:service_version</code> — 
-    #     The service version.  Only used if <code>api_method</code> is a
-    #     <code>String</code>.  Defaults to <code>'v1'</code>.
-    #   - <code>:parser</code> — 
-    #     The parser for the response.
-    #   - <code>:authorization</code> — 
-    #     The authorization mechanism for the response.  Used only if
-    #     <code>:signed</code> is <code>true</code>.
-    #   - <code>:signed</code> — 
-    #     <code>true</code> if the request must be signed, <code>false</code>
-    #     otherwise.  Defaults to <code>true</code> if an authorization
-    #     mechanism has been set, <code>false</code> otherwise.
-    #
-    # @return [Array] The generated request.
+    # @option options [Hash, Array] :headers The HTTP headers for the request.
+    # @option options [String] :body The body of the request.
+    # @option options [String] :version ("v1")
+    #   The service version. Only used if `api_method` is a `String`.
+    # @option options [#generate_authenticated_request] :authorization
+    #   The authorization mechanism for the response. Used only if
+    #   `:authenticated` is `true`.
+    # @option options [TrueClass, FalseClass] :authenticated (true)
+    #   `true` if the request must be signed or somehow
+    #   authenticated, `false` otherwise.
+    #
+    # @return [Google::APIClient::Reference] The generated request.
     #
     # @example
     #   request = client.generate_request(
-    #     'chili.activities.list',
-    #     {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+    #     :api_method => 'plus.activities.list',
+    #     :parameters =>
+    #       {'collection' => 'public', 'userId' => 'me'}
     #   )
-    #   method, uri, headers, body = request
-    def generate_request(
-        api_method, parameters={}, body='', headers=[], options={})
-      options={
-        :parser => self.parser,
-        :service_version => 'v1',
-        :authorization => self.authorization
+    def generate_request(options={})
+      options = {
+        :api_client => self
       }.merge(options)
-      # The default value for the :signed option depends on whether an
-      # authorization mechanism has been set.
-      if options[:authorization]
-        options = {:signed => true}.merge(options)
-      else
-        options = {:signed => false}.merge(options)
-      end
-      if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
-        api_method = self.discovered_method(
-          api_method.to_s, options[:service_version]
-        )
-      elsif !api_method.kind_of?(::Google::APIClient::Method)
-        raise TypeError,
-          "Expected String, Symbol, or Google::APIClient::Method, " +
-          "got #{api_method.class}."
-      end
-      unless api_method
-        raise ArgumentError, "API method could not be found."
-      end
-      request = api_method.generate_request(parameters, body, headers)
-      if options[:signed]
-        request = self.sign_request(request, options[:authorization])
-      end
-      return request
+      return Google::APIClient::Request.new(options)
     end
 
     ##
-    # Generates a request and transmits it.
+    # Executes a request, wrapping it in a Result object.
     #
-    # @param [Google::APIClient::Method, String] api_method
-    #   The method object or the RPC name of the method being executed.
-    # @param [Hash, Array] parameters
-    #   The parameters to send to the method.
-    # @param [String] body The body of the request.
-    # @param [Hash, Array] headers The HTTP headers for the request.
-    # @param [Hash] options
-    #   The configuration parameters for the request.
-    #   - <code>:service_version</code> — 
-    #     The service version.  Only used if <code>api_method</code> is a
-    #     <code>String</code>.  Defaults to <code>'v1'</code>.
-    #   - <code>:adapter</code> — 
-    #     The HTTP adapter.
-    #   - <code>:parser</code> — 
-    #     The parser for the response.
-    #   - <code>:authorization</code> — 
-    #     The authorization mechanism for the response.  Used only if
-    #     <code>:signed</code> is <code>true</code>.
-    #   - <code>:signed</code> — 
-    #     <code>true</code> if the request must be signed, <code>false</code>
-    #     otherwise.  Defaults to <code>true</code>.
-    #
-    # @return [Array] The response from the API.
+    # @param [Google::APIClient::Request, Hash, Array] params
+    #   Either a Google::APIClient::Request, a Hash, or an Array.
+    #
+    #   If a Google::APIClient::Request, no other parameters are expected.
+    #
+    #   If a Hash, the below parameters are handled. If an Array, the
+    #   parameters are assumed to be in the below order:
+    #
+    #   - (Google::APIClient::Method) api_method:
+    #     The method object or the RPC name of the method being executed.
+    #   - (Hash, Array) parameters:
+    #     The parameters to send to the method.
+    #   - (String) body: The body of the request.
+    #   - (Hash, Array) headers: The HTTP headers for the request.
+    #   - (Hash) options: A set of options for the request, of which:
+    #     - (#generate_authenticated_request) :authorization (default: true) -
+    #       The authorization mechanism for the response. Used only if
+    #       `:authenticated` is `true`.
+    #     - (TrueClass, FalseClass) :authenticated (default: true) -
+    #       `true` if the request must be signed or somehow
+    #       authenticated, `false` otherwise.
+    #
+    # @return [Google::APIClient::Result] The result from the API, nil if batch.
+    #
+    # @example
+    #   result = client.execute(batch_request)
     #
     # @example
-    #   response = client.execute(
-    #     'chili.activities.list',
-    #     {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+    #   plus = client.discovered_api('plus')
+    #   result = client.execute(
+    #     :api_method => plus.activities.list,
+    #     :parameters => {'collection' => 'public', 'userId' => 'me'}
     #   )
-    #   status, headers, body = response
-    def execute(api_method, parameters={}, body='', headers=[], options={})
-      request = self.generate_request(
-        api_method, parameters, body, headers, options
-      )
-      return self.transmit_request(
-        request,
-        options[:adapter] || self.http_adapter
-      )
+    #
+    # @see Google::APIClient#generate_request
+    def execute(*params)
+      if params.last.kind_of?(Google::APIClient::Request) &&
+          params.size == 1
+        request = params.pop
+        options = {}
+      else
+        # This block of code allows us to accept multiple parameter passing
+        # styles, and maintaining some backwards compatibility.
+        #
+        # Note: I'm extremely tempted to deprecate this style of execute call.
+        if params.last.respond_to?(:to_hash) && params.size == 1
+          options = params.pop
+        else
+          options = {}
+        end
+
+        options[:api_method] = params.shift if params.size > 0
+        options[:parameters] = params.shift if params.size > 0
+        options[:body] = params.shift if params.size > 0
+        options[:headers] = params.shift if params.size > 0
+        options.update(params.shift) if params.size > 0
+        request = self.generate_request(options)
+      end
+      
+      request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
+      request.parameters['key'] ||= self.key unless self.key.nil?
+      request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
+
+      connection = options[:connection] || Faraday.default_connection
+      request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
+
+      result = request.send(connection)
+      if result.status == 401 && authorization.respond_to?(:refresh_token) && auto_refresh_token
+        begin
+          logger.debug("Attempting refresh of access token & retry of request")
+          authorization.fetch_access_token!
+          result = request.send(connection)
+        rescue Signet::AuthorizationError
+           # Ignore since we want the original error
+        end
+      end
+      
+      return result
     end
 
     ##
-    # Transmits the request using the current HTTP adapter.
-    #
-    # @param [Array] request The request to transmit.
-    # @param [#transmit] adapter The HTTP adapter.
+    # Same as Google::APIClient#execute, but raises an exception if there was
+    # an error.
     #
-    # @return [Array] The response from the server.
-    def transmit_request(request, adapter=self.http_adapter)
-      ::HTTPAdapter.transmit(request, adapter)
+    # @see Google::APIClient#execute
+    def execute!(*params)
+      result = self.execute(*params)
+      if result.error?
+        error_message = result.error_message
+        case result.response.status
+          when 400...500
+            exception_type = ClientError
+            error_message ||= "A client error has occurred."
+          when 500...600
+            exception_type = ServerError
+            error_message ||= "A server error has occurred."
+          else
+            exception_type = TransmissionError
+            error_message ||= "A transmission error has occurred."
+        end
+        raise exception_type, error_message
+      end
+      return result
     end
-
+    
+    protected
+    
     ##
-    # Signs a request using the current authorization mechanism.
-    #
-    # @param [Array] request The request to sign.
-    # @param [#generate_authenticated_request] authorization
-    #   The authorization mechanism.
+    # Resolves a URI template against the client's configured base.
     #
-    # @return [Array] The signed request.
-    def sign_request(request, authorization=self.authorization)
-      return authorization.generate_authenticated_request(
-        :request => request
-      )
+    # @api private
+    # @param [String, Addressable::URI, Addressable::Template] template
+    #   The template to resolve.
+    # @param [Hash] mapping The mapping that corresponds to the template.
+    # @return [Addressable::URI] The expanded URI.
+    def resolve_uri(template, mapping={})
+      @base_uri ||= Addressable::URI.new(
+        :scheme => 'https',
+        :host => self.host,
+        :port => self.port
+      ).normalize
+      template = if template.kind_of?(Addressable::Template)
+        template.pattern
+      elsif template.respond_to?(:to_str)
+        template.to_str
+      else
+        raise TypeError,
+          "Expected String, Addressable::URI, or Addressable::Template, " +
+          "got #{template.class}."
+      end
+      return Addressable::Template.new(@base_uri + template).expand(mapping)
     end
+    
   end
 end