Merge remote-tracking branch 'origin/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 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     # 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 unpack_api_response(j, kind=nil)
94     if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/)
95       ary = j[:items].collect { |x| unpack_api_response x, j[:kind] }
96       if j[:items_available]
97         (class << ary; self; end).class_eval { attr_accessor :items_available }
98         ary.items_available = j[:items_available]
99       end
100       ary
101     elsif j.is_a? Hash and (kind || j[:kind])
102       oclass = self.kind_class(kind || j[:kind])
103       if oclass
104         j.keys.each do |k|
105           childkind = j["#{k.to_s}_kind".to_sym]
106           if childkind
107             j[k] = self.unpack_api_response(j[k], childkind)
108           end
109         end
110         oclass.new.private_reload(j)
111       else
112         j
113       end
114     else
115       j
116     end
117   end
118
119   def arvados_login_url(params={})
120     if Rails.configuration.respond_to? :arvados_login_base
121       uri = Rails.configuration.arvados_login_base
122     else
123       uri = self.arvados_v1_base.sub(%r{/arvados/v\d+.*}, '/login')
124     end
125     if params.size > 0
126       uri += '?' << params.collect { |k,v|
127         CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s)
128       }.join('&')
129     end
130   end
131
132   def arvados_logout_url(params={})
133     arvados_login_url(params).sub('/login','/logout')
134   end
135
136   def arvados_v1_base
137     Rails.configuration.arvados_v1_base
138   end
139
140   def arvados_schema
141     @arvados_schema ||= api 'schema', ''
142   end
143
144   def discovery
145     @discovery ||= api '../../discovery/v1/apis/arvados/v1/rest', ''
146   end
147
148   def kind_class(kind)
149     kind.match(/^arvados\#(.+?)(_list|List)?$/)[1].pluralize.classify.constantize rescue nil
150   end
151
152   def class_kind(resource_class)
153     resource_class.to_s.underscore
154   end
155
156   protected
157   def profile_checkpoint label=nil
158     return if !@@profiling_enabled
159     label = yield if block_given?
160     t = Time.now
161     if label and @profile_t0
162       Rails.logger.info "API client: #{t - @profile_t0} #{label}"
163     end
164     @profile_t0 = t
165   end
166 end