2756: Fix "duplicate tab" behavior in Chrome.
[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
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     resources_kind = class_kind(resources_kind).pluralize if resources_kind.is_a? Class
30     url = "#{self.arvados_v1_base}/#{resources_kind}#{action}"
31
32     # Clean up /arvados/v1/../../discovery/v1 to /discovery/v1
33     url.sub! '/arvados/v1/../../', '/'
34
35     query = {
36       'api_token' => Thread.current[:arvados_api_token] || '',
37       'reader_tokens' => (Thread.current[:reader_tokens] || []).to_json,
38     }
39     if !data.nil?
40       data.each do |k,v|
41         if v.is_a? String or v.nil?
42           query[k] = v
43         elsif v == true
44           query[k] = 1
45         elsif v == false
46           query[k] = 0
47         else
48           query[k] = JSON.dump(v)
49         end
50       end
51     else
52       query["_method"] = "GET"
53     end
54     if @@profiling_enabled
55       query["_profile"] = "true"
56     end
57
58     header = {"Accept" => "application/json"}
59
60     profile_checkpoint { "Prepare request #{url} #{query[:uuid]} #{query[:where]} #{query[:filters]}" }
61     msg = @@api_client.post(url,
62                             query,
63                             header: header)
64     profile_checkpoint 'API transaction'
65
66     if msg.status_code == 401
67       raise NotLoggedInException.new
68     end
69
70     json = msg.content
71
72     begin
73       resp = Oj.load(json, :symbol_keys => true)
74     rescue Oj::ParseError
75       raise InvalidApiResponseException.new json
76     end
77     if not resp.is_a? Hash
78       raise InvalidApiResponseException.new json
79     end
80     if msg.status_code != 200
81       errors = resp[:errors]
82       errors = errors.join("\n\n") if errors.is_a? Array
83       raise "#{errors} [API: #{msg.status_code}]"
84     end
85     if resp[:_profile]
86       Rails.logger.info "API client: " \
87       "#{resp.delete(:_profile)[:request_time]} request_time"
88     end
89     profile_checkpoint 'Parse response'
90     resp
91   end
92
93   def self.patch_paging_vars(ary, items_available, offset, limit, links=nil)
94     if items_available
95       (class << ary; self; end).class_eval { attr_accessor :items_available }
96       ary.items_available = items_available
97     end
98     if offset
99       (class << ary; self; end).class_eval { attr_accessor :offset }
100       ary.offset = offset
101     end
102     if limit
103       (class << ary; self; end).class_eval { attr_accessor :limit }
104       ary.limit = limit
105     end
106     if links
107       (class << ary; self; end).class_eval { attr_accessor :links }
108       ary.links = links
109     end
110     ary
111   end
112
113   def unpack_api_response(j, kind=nil)
114     if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/)
115       ary = j[:items].collect { |x| unpack_api_response x, x[:kind] }
116       links = ArvadosResourceList.new Link
117       links.results = (j[:links] || []).collect do |x|
118         unpack_api_response x, x[:kind]
119       end
120       self.class.patch_paging_vars(ary, j[:items_available], j[:offset], j[:limit], links)
121     elsif j.is_a? Hash and (kind || j[:kind])
122       oclass = self.kind_class(kind || j[:kind])
123       if oclass
124         j.keys.each do |k|
125           childkind = j["#{k.to_s}_kind".to_sym]
126           if childkind
127             j[k] = self.unpack_api_response(j[k], childkind)
128           end
129         end
130         oclass.new.private_reload(j)
131       else
132         j
133       end
134     else
135       j
136     end
137   end
138
139   def arvados_login_url(params={})
140     if Rails.configuration.respond_to? :arvados_login_base
141       uri = Rails.configuration.arvados_login_base
142     else
143       uri = self.arvados_v1_base.sub(%r{/arvados/v\d+.*}, '/login')
144     end
145     if params.size > 0
146       uri += '?' << params.collect { |k,v|
147         CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s)
148       }.join('&')
149     end
150   end
151
152   def arvados_logout_url(params={})
153     arvados_login_url(params).sub('/login','/logout')
154   end
155
156   def arvados_v1_base
157     Rails.configuration.arvados_v1_base
158   end
159
160   def discovery
161     @discovery ||= api '../../discovery/v1/apis/arvados/v1/rest', ''
162   end
163
164   def kind_class(kind)
165     kind.match(/^arvados\#(.+?)(_list|List)?$/)[1].pluralize.classify.constantize rescue nil
166   end
167
168   def class_kind(resource_class)
169     resource_class.to_s.underscore
170   end
171
172   protected
173   def profile_checkpoint label=nil
174     return if !@@profiling_enabled
175     label = yield if block_given?
176     t = Time.now
177     if label and @profile_t0
178       Rails.logger.info "API client: #{t - @profile_t0} #{label}"
179     end
180     @profile_t0 = t
181   end
182 end