Merge branch '1788-support-and-feedback'
[arvados.git] / apps / workbench / app / models / arvados_api_client.rb
1 require 'httpclient'
2 require 'thread'
3
4 class ArvadosApiClient
5   class NotLoggedInException < StandardError
6   end
7   class InvalidApiResponseException < StandardError
8   end
9
10   @@client_mtx = Mutex.new
11   @@api_client = nil
12   @@profiling_enabled = Rails.configuration.profiling_enabled rescue false
13
14   def api(resources_kind, action, data=nil)
15     profile_checkpoint
16
17     @@client_mtx.synchronize do
18       if not @@api_client 
19         @@api_client = HTTPClient.new
20         if Rails.configuration.arvados_insecure_https
21           @@api_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
22         else
23           # Use system CA certificates
24           @@api_client.ssl_config.add_trust_ca('/etc/ssl/certs')
25         end
26       end
27     end
28
29     api_token = Thread.current[:arvados_api_token]
30     api_token ||= ''
31
32     resources_kind = class_kind(resources_kind).pluralize if resources_kind.is_a? Class
33     url = "#{self.arvados_v1_base}/#{resources_kind}#{action}"
34
35     query = {"api_token" => api_token}
36     if !data.nil?
37       data.each do |k,v|
38         if v.is_a? String or v.nil?
39           query[k] = v
40         elsif v == true
41           query[k] = 1
42         elsif v == false
43           query[k] = 0
44         else
45           query[k] = JSON.dump(v)
46         end
47       end
48     else
49       query["_method"] = "GET"
50     end
51     if @@profiling_enabled
52       query["_profile"] = "true"
53     end
54     
55     header = {"Accept" => "application/json"}
56
57     profile_checkpoint { "Prepare request #{url} #{query[:uuid]} #{query[:where]}" }
58     msg = @@api_client.post(url, 
59                             query,
60                             header: header)
61     profile_checkpoint 'API transaction'
62
63     if msg.status_code == 401
64       raise NotLoggedInException.new
65     end
66
67     json = msg.content
68     
69     begin
70       resp = Oj.load(json, :symbol_keys => true)
71     rescue Oj::ParseError
72       raise InvalidApiResponseException.new json
73     end
74     if not resp.is_a? Hash
75       raise InvalidApiResponseException.new json
76     end
77     if msg.status_code != 200
78       errors = resp[:errors]
79       errors = errors.join("\n\n") if errors.is_a? Array
80       raise "API error #{msg.status_code}:\n\n#{errors}\n"
81     end
82     if resp[:_profile]
83       Rails.logger.info "API client: " \
84       "#{resp.delete(:_profile)[:request_time]} request_time"
85     end
86     profile_checkpoint 'Parse response'
87     resp
88   end
89
90   def unpack_api_response(j, kind=nil)
91     if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/)
92       ary = j[:items].collect { |x| unpack_api_response x, j[:kind] }
93       if j[:items_available]
94         (class << ary; self; end).class_eval { attr_accessor :items_available }
95         ary.items_available = j[:items_available]
96       end
97       ary
98     elsif j.is_a? Hash and (kind || j[:kind])
99       oclass = self.kind_class(kind || j[:kind])
100       if oclass
101         j.keys.each do |k|
102           childkind = j["#{k.to_s}_kind".to_sym]
103           if childkind
104             j[k] = self.unpack_api_response(j[k], childkind)
105           end
106         end
107         oclass.new.private_reload(j)
108       else
109         j
110       end
111     else
112       j
113     end
114   end
115
116   def arvados_login_url(params={})
117     if Rails.configuration.respond_to? :arvados_login_base
118       uri = Rails.configuration.arvados_login_base
119     else
120       uri = self.arvados_v1_base.sub(%r{/arvados/v\d+.*}, '/login')
121     end
122     if params.size > 0
123       uri += '?' << params.collect { |k,v|
124         CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s)
125       }.join('&')
126     end
127   end
128
129   def arvados_logout_url(params={})
130     arvados_login_url(params).sub('/login','/logout')
131   end
132
133   def arvados_v1_base
134     Rails.configuration.arvados_v1_base
135   end
136
137   def arvados_schema
138     @arvados_schema ||= api 'schema', ''
139   end
140
141   def kind_class(kind)
142     kind.match(/^arvados\#(.+?)(_list|List)?$/)[1].pluralize.classify.constantize rescue nil
143   end
144
145   def class_kind(resource_class)
146     resource_class.to_s.underscore
147   end
148
149   protected
150   def profile_checkpoint label=nil
151     return if !@@profiling_enabled
152     label = yield if block_given?
153     t = Time.now
154     if label and @profile_t0
155       Rails.logger.info "API client: #{t - @profile_t0} #{label}"
156     end
157     @profile_t0 = t
158   end
159 end