Merge branch 'master' into 1971-show-image-thumbnails
[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     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     # Clean up /arvados/v1/../../discovery/v1 to /discovery/v1
36     url.sub! '/arvados/v1/../../', '/'
37
38     query = {"api_token" => api_token}
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]}" }
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)
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     ary
107   end
108
109   def unpack_api_response(j, kind=nil)
110     if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/)
111       ary = j[:items].collect { |x| unpack_api_response x, j[:kind] }
112       ArvadosApiClient::patch_paging_vars(ary, j[:items_available], j[:offset], j[:limit])
113     elsif j.is_a? Hash and (kind || j[:kind])
114       oclass = self.kind_class(kind || j[:kind])
115       if oclass
116         j.keys.each do |k|
117           childkind = j["#{k.to_s}_kind".to_sym]
118           if childkind
119             j[k] = self.unpack_api_response(j[k], childkind)
120           end
121         end
122         oclass.new.private_reload(j)
123       else
124         j
125       end
126     else
127       j
128     end
129   end
130
131   def arvados_login_url(params={})
132     if Rails.configuration.respond_to? :arvados_login_base
133       uri = Rails.configuration.arvados_login_base
134     else
135       uri = self.arvados_v1_base.sub(%r{/arvados/v\d+.*}, '/login')
136     end
137     if params.size > 0
138       uri += '?' << params.collect { |k,v|
139         CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s)
140       }.join('&')
141     end
142   end
143
144   def arvados_logout_url(params={})
145     arvados_login_url(params).sub('/login','/logout')
146   end
147
148   def arvados_v1_base
149     Rails.configuration.arvados_v1_base
150   end
151
152   def discovery
153     @discovery ||= api '../../discovery/v1/apis/arvados/v1/rest', ''
154   end
155
156   def kind_class(kind)
157     kind.match(/^arvados\#(.+?)(_list|List)?$/)[1].pluralize.classify.constantize rescue nil
158   end
159
160   def class_kind(resource_class)
161     resource_class.to_s.underscore
162   end
163
164   protected
165   def profile_checkpoint label=nil
166     return if !@@profiling_enabled
167     label = yield if block_given?
168     t = Time.now
169     if label and @profile_t0
170       Rails.logger.info "API client: #{t - @profile_t0} #{label}"
171     end
172     @profile_t0 = t
173   end
174 end