Use discovery document to build infix->kind map instead of using a
[arvados.git] / apps / workbench / app / models / arvados_api_client.rb
index e47cb315ce629a8477991fe0ab387f2c2f79b3de..b2d774cac493be43a1f27a2226f657c0b5f6099e 100644 (file)
+require 'httpclient'
+require 'thread'
+
 class ArvadosApiClient
   class NotLoggedInException < StandardError
   end
   class InvalidApiResponseException < StandardError
   end
+
+  @@client_mtx = Mutex.new
+  @@api_client = nil
+  @@profiling_enabled = Rails.configuration.profiling_enabled rescue false
+
   def api(resources_kind, action, data=nil)
-    arvados_api_token = Thread.current[:arvados_api_token]
-    arvados_api_token = '' if arvados_api_token.nil?
-    dataargs = ['--data-urlencode',
-                "api_token=#{arvados_api_token}",
-                '--header',
-                'Accept:application/json']
+    profile_checkpoint
+
+    @@client_mtx.synchronize do
+      if not @@api_client 
+        @@api_client = HTTPClient.new
+        if Rails.configuration.arvados_insecure_https
+          @@api_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
+        else
+          # Use system CA certificates
+          @@api_client.ssl_config.add_trust_ca('/etc/ssl/certs')
+        end
+      end
+    end
+
+    api_token = Thread.current[:arvados_api_token]
+    api_token ||= ''
+
+    resources_kind = class_kind(resources_kind).pluralize if resources_kind.is_a? Class
+    url = "#{self.arvados_v1_base}/#{resources_kind}#{action}"
+
+    query = {"api_token" => api_token}
     if !data.nil?
       data.each do |k,v|
-        dataargs << '--data-urlencode'
         if v.is_a? String or v.nil?
-          dataargs << "#{k}=#{v}"
-        elsif v == true or v == false
-          dataargs << "#{k}=#{v ? 1 : 0}"
+          query[k] = v
+        elsif v == true
+          query[k] = 1
+        elsif v == false
+          query[k] = 0
         else
-          dataargs << "#{k}=#{JSON.dump(v)}"
+          query[k] = JSON.dump(v)
         end
       end
     else
-      dataargs << '--data-urlencode' << '_method=GET'
+      query["_method"] = "GET"
     end
-    json = nil
-    resources_kind = class_kind(resources_kind).pluralize if resources_kind.is_a? Class
-    url = "#{self.arvados_v1_base}/#{resources_kind}#{action}"
-    IO.popen([ENV,
-              'curl',
-              "-s#{'k' if Rails.configuration.arvados_insecure_https}",
-              *dataargs,
-              url],
-             'r') do |io|
-      json = io.read
+    if @@profiling_enabled
+      query["_profile"] = "true"
     end
+    
+    header = {"Accept" => "application/json"}
+
+    profile_checkpoint { "Prepare request #{url} #{query[:uuid]} #{query[:where]}" }
+    msg = @@api_client.post(url, 
+                            query,
+                            header: header)
+    profile_checkpoint 'API transaction'
+
+    if msg.status_code == 401
+      raise NotLoggedInException.new
+    end
+
+    json = msg.content
+    
     begin
       resp = Oj.load(json, :symbol_keys => true)
     rescue Oj::ParseError
       raise InvalidApiResponseException.new json
     end
-    if resp[:errors]
-      if resp[:errors][0] == 'Not logged in'
-        raise NotLoggedInException.new
-      else
-        errors = resp[:errors]
-        errors = errors.join("\n\n") if errors.is_a? Array
-        raise "API errors:\n\n#{errors}\n"
-      end
+    if not resp.is_a? Hash
+      raise InvalidApiResponseException.new json
+    end
+    if msg.status_code != 200
+      errors = resp[:errors]
+      errors = errors.join("\n\n") if errors.is_a? Array
+      raise "#{errors} [API: #{msg.status_code}]"
     end
+    if resp[:_profile]
+      Rails.logger.info "API client: " \
+      "#{resp.delete(:_profile)[:request_time]} request_time"
+    end
+    profile_checkpoint 'Parse response'
     resp
   end
 
   def unpack_api_response(j, kind=nil)
     if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/)
-      j[:items].collect { |x| unpack_api_response x, j[:kind] }
+      ary = j[:items].collect { |x| unpack_api_response x, j[:kind] }
+      if j[:items_available]
+        (class << ary; self; end).class_eval { attr_accessor :items_available }
+        ary.items_available = j[:items_available]
+      end
+      ary
     elsif j.is_a? Hash and (kind || j[:kind])
       oclass = self.kind_class(kind || j[:kind])
       if oclass
@@ -98,6 +138,10 @@ class ArvadosApiClient
     @arvados_schema ||= api 'schema', ''
   end
 
+  def discovery
+    @discovery ||= api '../../discovery/v1/apis/arvados/v1/rest', ''
+  end
+
   def kind_class(kind)
     kind.match(/^arvados\#(.+?)(_list|List)?$/)[1].pluralize.classify.constantize rescue nil
   end
@@ -105,4 +149,15 @@ class ArvadosApiClient
   def class_kind(resource_class)
     resource_class.to_s.underscore
   end
+
+  protected
+  def profile_checkpoint label=nil
+    return if !@@profiling_enabled
+    label = yield if block_given?
+    t = Time.now
+    if label and @profile_t0
+      Rails.logger.info "API client: #{t - @profile_t0} #{label}"
+    end
+    @profile_t0 = t
+  end
 end