require 'json' require 'net/ftptls' # silence the parenthesis warning in rest-client 1.6.1 old_verbose, $VERBOSE = $VERBOSE, nil ; require 'rest-client' ; $VERBOSE = old_verbose module GoodData # = GoodData HTTP wrapper # # Provides a convenient HTTP wrapper for talking with the GoodData API. # # Remember that the connection is shared amongst the entire application. # Therefore you can't be logged in to more than _one_ GoodData account. # per session. Simultaneous connections to multiple GoodData accounts is not # supported at this time. # # The GoodData API is a RESTful API that communicates using JSON. This wrapper # makes sure that the session is stored between requests and that the JSON is # parsed both when sending and receiving. # # == Usage # # Before a connection can be made to the GoodData API, you have to supply the user # credentials using the set_credentials method: # # Connection.new(username, password).set_credentials(username, password) # # To send a HTTP request use either the get, post or delete methods documented below. # class Connection DEFAULT_URL = 'https://secure.gooddata.com' LOGIN_PATH = '/gdc/account/login' TOKEN_PATH = '/gdc/account/token' # Set the GoodData account credentials. # # This have to be performed before any calls to the API. # # === Parameters # # * +username+ - The GoodData account username # * +password+ - The GoodData account password def initialize(username, password, url = nil, options = {}) @status = :not_connected @username = username @password = password @url = url || DEFAULT_URL @options = options end # Returns the user JSON object of the currently logged in GoodData user account. def user ensure_connection @user end # Performs a HTTP GET request. # # Retuns the JSON response formatted as a Hash object. # # === Parameters # # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash) # # === Examples # # Connection.new(username, password).get '/gdc/projects' def get(path, options = {}) GoodData.logger.debug "GET #{path}" ensure_connection process_response(options) { @server[path].get cookies } end # Performs a HTTP POST request. # # Retuns the JSON response formatted as a Hash object. # # === Parameters # # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash) # * +data+ - The payload data in the format of a Hash object # # === Examples # # Connection.new(username, password).post '/gdc/projects', { ... } def post(path, data, options = {}) payload = data.to_json GoodData.logger.debug "POST #{path}, payload: #{payload}" ensure_connection process_response(options) { @server[path].post payload, cookies } end # Performs a HTTP DELETE request. # # Retuns the JSON response formatted as a Hash object. # # === Parameters # # * +path+ - The HTTP path on the GoodData server (must be prefixed with a forward slash) # # === Examples # # Connection.new(username, password).delete '/gdc/project/1' def delete(path) GoodData.logger.debug "DELETE #{path}" ensure_connection process_response { @server[path].delete cookies } end # Get the cookies associated with the current connection. def cookies @cookies ||= { :cookies => {} } end # Set the cookies used when communicating with the GoodData API. def merge_cookies!(cookies) self.cookies @cookies[:cookies].merge! cookies end # Returns true if a connection have been established to the GoodData API # and the login was successful. def logged_in? @status == :logged_in end # The connection will automatically be established once it's needed, which it # usually is when either the user, get, post or delete method is called. If you # want to force a connection (or a re-connect) you can use this method. def connect! connect end # Uploads a file to GoodData server via FTPS def upload(file, dir = nil) Net::FTPTLS.open('secure-di.gooddata.com', @username, @password) do |ftp| ftp.passive = true if dir then begin ; ftp.mkdir dir ; rescue ; ensure ; ftp.chdir dir ; end end ftp.binary = true ftp.put file end end private def ensure_connection connect if @status == :not_connected end def connect # GoodData.logger.info "Connecting to GoodData..." @status = :connecting authenticate end def authenticate credentials = { 'postUserLogin' => { 'login' => @username, 'password' => @password, 'remember' => 1 } } @server = RestClient::Resource.new @url, :timeout => @options[:timeout], :headers => { :content_type => :json, :accept => [ :json, :zip ], :user_agent => GoodData.gem_version_string, } GoodData.logger.debug "Logging in..." @user = post(LOGIN_PATH, credentials, :dont_reauth => true)['userLogin'] refresh_token :dont_reauth => true # avoid infinite loop if refresh_token fails with 401 @status = :logged_in end def process_response(options = {}) begin begin response = yield rescue RestClient::Unauthorized raise $! if options[:dont_reauth] refresh_token response = yield end merge_cookies! response.cookies content_type = response.headers[:content_type] if content_type == "application/json" then result = response.to_str == '""' ? {} : JSON.parse(response.to_str) GoodData.logger.debug "Response: #{result.inspect}" elsif content_type == "application/zip" then result = response GoodData.logger.debug "Response: a zipped stream" elsif response.headers[:content_length].to_s == '0' result = nil else raise "Unsupported response content type '%s':\n%s" % [ content_type, response.to_str[0..127] ] end result rescue RestClient::Exception => e GoodData.logger.debug "Response: #{e.response}" raise $! end end def refresh_token(options = {}) GoodData.logger.debug "Getting authentication token..." begin get TOKEN_PATH, :dont_reauth => true # avoid infinite loop GET fails with 401 rescue RestClient::Unauthorized raise $! if options[:dont_reauth] authenticate end end end end