omf_sfa 0.2.6 → 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c8e057cf74d9cfbe634681eb5a86a56d1b06f48e
4
+ data.tar.gz: df5ac1030bab27d3db06ff6bd67e72c899ae75ee
5
+ SHA512:
6
+ metadata.gz: 6e561cdb795206908e2dc0f32cc58be10af3e5cb959bfafdf89f6b3be285ee470e92c46ce012b11dad2749ae998356aab1576fc71cffcc80272104eeb3100c59
7
+ data.tar.gz: dfbc2f4d3e40a9c4895c7ea6479d3e5dc7cd2a7df030522ea257aac78a8649e856745130048455501c067b031a70359800a1380df97d6bf79bd79a720082d1c9
data/.gitignore CHANGED
@@ -22,3 +22,4 @@ Gemfile.lock
22
22
  .project
23
23
  .DS_Store
24
24
  *.original
25
+ .idea
File without changes
@@ -3,5 +3,8 @@
3
3
  require 'omf_sfa'
4
4
 
5
5
  module OMF::SFA
6
- module AM; end
6
+ module AM
7
+ class AMException < Exception; end
8
+ end
7
9
  end
10
+
@@ -0,0 +1,119 @@
1
+
2
+ module OMF::SFA::AM::Rest
3
+
4
+ class PromiseHandler < RestHandler
5
+
6
+ @@contexts = {}
7
+
8
+ def self.register_promise(promise, uuid = nil, html_reply = false, collection_set = nil)
9
+ debug "Registering promise '#{uuid}' - #{promise}"
10
+ uuid ||= UUIDTools::UUID.random_create
11
+ #uuid = 1 # TODO: REMOVE
12
+ @@contexts[uuid.to_s] = {
13
+ promise: promise,
14
+ html_reply: html_reply,
15
+ collection_set: collection_set || Set.new,
16
+ timestamp: Time.now
17
+ }
18
+ path = "/promises/#{uuid}"
19
+ debug "Redirecting to #{path}"
20
+ return [302, {'Location' => path}, ['Promised, but not ready yet.']]
21
+ end
22
+
23
+ def call(env)
24
+ begin
25
+ req = ::Rack::Request.new(env)
26
+ headers = {
27
+ 'Access-Control-Allow-Origin' => '*',
28
+ 'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
29
+ 'Access-Control-Allow-Headers' => 'origin, x-csrftoken, content-type, accept'
30
+ }
31
+ if req.request_method == 'OPTIONS'
32
+ return [200 , headers, ""]
33
+ end
34
+
35
+ uuid = req.path_info.split('/')[-1]
36
+ debug "Checking for promise '#{uuid}'"
37
+
38
+ unless context = @@contexts[uuid]
39
+ return [404, {}, "Can't find requested promise"]
40
+ end
41
+ context[:timestamp] = Time.now # keep reaper away
42
+ promise = context[:promise]
43
+ case promise.status
44
+ when :pending
45
+ return retry_later(req, context)
46
+ when :rejected
47
+ body = {
48
+ type: 'error',
49
+ error: {
50
+ reason: promise.error_msg,
51
+ code: promise.error_code
52
+ }
53
+ }
54
+ headers['Content-Type'] = 'application/json'
55
+ return [500, headers, JSON.pretty_generate(body)]
56
+ end
57
+
58
+ # OK, the promise seems to have been resolved
59
+ content_type, body = promise.value
60
+ #puts ">>>>VALUE #{body.class} - #{content_type}>>>>> #{body}"
61
+ if body.is_a? Thin::AsyncResponse
62
+ return body.finish
63
+ end
64
+
65
+ begin
66
+ if content_type == 'text/html'
67
+ #body = self.class.convert_to_html(body, env, Set.new((@coll_handlers || {}).keys))
68
+ body = convert_to_html(body, env, {}, context[:collection_set])
69
+ else
70
+ content_type = 'application/json'
71
+ body = JSON.pretty_generate(body)
72
+ end
73
+ rescue OMF::SFA::Util::PromiseUnresolvedException => pex
74
+ proxy = OMF::SFA::Util::Promise.new
75
+ pex.promise.on_success do |d|
76
+ proxy.resolve [content_type, body]
77
+ end.on_error(proxy)
78
+ uuid = pex.uuid
79
+ path = "/promises/#{uuid}"
80
+ require 'omf-sfa/am/am-rest/promise_handler' # delay loading as PromiseHandler sub classes this class
81
+ self.class.register_promise(proxy, uuid, context[:html_reply], context[:collection_set])
82
+ debug "Redirecting to #{path}"
83
+ return [302, {'Location' => path}, ['Promised, but not ready yet.']]
84
+ end
85
+ headers['Content-Type'] = content_type
86
+ return [200 , headers, (body || '') + "\n"]
87
+
88
+ rescue RackException => rex
89
+ return rex.reply
90
+ rescue Exception => ex
91
+ body = {
92
+ type: 'error',
93
+ error: {
94
+ reason: ex.to_s,
95
+ bt: ex.backtrace #.select {|l| !l.start_with?('/') }
96
+ }
97
+ }
98
+ warn "ERROR: #{ex}"
99
+ debug ex.backtrace.join("\n")
100
+ headers['Content-Type'] = 'application/json'
101
+ return [500, headers, JSON.pretty_generate(body)]
102
+ end
103
+ end
104
+
105
+ def retry_later(req, context)
106
+ body = {
107
+ type: 'retry',
108
+ delay: 10, # rex.delay,
109
+ url: absolute_path(req.env['REQUEST_PATH']),
110
+ progress: context[:promise].progress.map do |ts, msg|
111
+ "#{ts.utc.iso8601}: #{msg}"
112
+ end
113
+ }
114
+ debug "Retry later request - #{req.url}"
115
+ headers = {'Content-Type' => 'application/json'}
116
+ return [504, headers, JSON.pretty_generate(body)]
117
+ end
118
+ end
119
+ end
@@ -8,6 +8,7 @@ require 'thin/async'
8
8
  require 'cgi'
9
9
 
10
10
  require 'omf_base/lobject'
11
+ require 'omf-sfa/util/promise'
11
12
 
12
13
 
13
14
  module OMF::SFA::AM::Rest
@@ -75,6 +76,12 @@ module OMF::SFA::AM::Rest
75
76
  end
76
77
  end
77
78
 
79
+ class TemporaryUnavailableException < RackException
80
+ def initialize()
81
+ super 504, "Upstream servers haven't responded yet, please try again a bit later"
82
+ end
83
+ end
84
+
78
85
  class RedirectException < Exception
79
86
  attr_reader :path
80
87
 
@@ -83,17 +90,68 @@ module OMF::SFA::AM::Rest
83
90
  end
84
91
  end
85
92
 
93
+ class ContentFoundException < Exception
94
+ def initialize(content, type = 'text', &block)
95
+ @type = case type.to_sym
96
+ when :json
97
+ 'application/json'
98
+ when :xml
99
+ 'text/xml'
100
+ when :text
101
+ 'text'
102
+ else
103
+ (type || 'text').to_s
104
+ end
105
+ if content.is_a? OMF::SFA::Util::Promise
106
+ @content = OMF::SFA::Util::Promise.new
107
+ content.on_success do |pv|
108
+ v = block ? block.call(pv) : pv
109
+ @content.resolve [@type, v]
110
+ end.on_error(@content).on_progress(@content)
111
+ else
112
+ @content = content
113
+ end
114
+ end
115
+
116
+ def reply
117
+ if @content.is_a? OMF::SFA::Util::Promise
118
+ require 'omf-sfa/am/am-rest/promise_handler' # delay loading as PromiseHandler sub classes this class
119
+ OMF::SFA::AM::Rest::PromiseHandler.register_promise(@content)
120
+ else
121
+ headers = {'Content-Type' => @type}
122
+ [200 , headers, @content]
123
+ end
124
+ end
125
+ end
126
+
127
+ # class PromiseUnresolvedException < Exception
128
+ # attr_reader :uuid, :promise
129
+ #
130
+ # def initialize(promise)
131
+ # @uuid = UUIDTools::UUID.random_create
132
+ # @promise = promise
133
+ # end
134
+ # end
135
+
86
136
  # Raised when a request triggers an async call whose
87
137
  # result we need before answering the request
88
138
  #
89
139
  class RetryLaterException < Exception
90
140
  attr_reader :delay
91
141
 
92
- def initialize(delay = 3)
142
+ def initialize(delay = 10)
93
143
  @delay = delay
94
144
  end
95
145
  end
96
146
 
147
+ # class FoundUnresolvedPromiseException < Exception
148
+ # attr_reader :promise
149
+ #
150
+ # def initialize(promise)
151
+ # @promise = promise
152
+ # end
153
+ # end
154
+
97
155
 
98
156
 
99
157
  class RestHandler < OMF::Base::LObject
@@ -147,40 +205,63 @@ module OMF::SFA::AM::Rest
147
205
  return [200 , headers, ""]
148
206
  end
149
207
  content_type, body = dispatch(req)
208
+ #puts "BODY(#{body.class}) - #{content_type}) >>>>> #{body}"
150
209
  if body.is_a? Thin::AsyncResponse
151
210
  return body.finish
152
211
  end
153
- if req['_format'] == 'html'
154
- #body = self.class.convert_to_html(body, env, Set.new((@coll_handlers || {}).keys))
155
- body = convert_to_html(body, env, {}, Set.new((@coll_handlers || {}).keys))
156
- content_type = 'text/html'
157
- elsif content_type == 'application/json'
158
- body = JSON.pretty_generate(body)
159
- end
212
+ content_type, body = _format_body(body, content_type, req, env)
213
+ # if req['_format'] == 'html'
214
+ # #body = self.class.convert_to_html(body, env, Set.new((@coll_handlers || {}).keys))
215
+ # body = convert_to_html(body, env, {}, Set.new((@coll_handlers || {}).keys))
216
+ # content_type = 'text/html'
217
+ # elsif content_type == 'application/json'
218
+ # body = JSON.pretty_generate(body)
219
+ # end
160
220
  #return [200 ,{'Content-Type' => content_type}, body + "\n"]
161
221
  headers['Content-Type'] = content_type
162
- return [200 , headers, body + "\n"]
222
+ return [200 , headers, (body || '') + "\n"]
223
+ rescue ContentFoundException => cex
224
+ return cex.reply
163
225
  rescue RackException => rex
226
+ unless rex.is_a? TemporaryUnavailableException
227
+ warn "Caught #{rex} - #{env['REQUEST_METHOD']} - #{env['REQUEST_PATH']}"
228
+ debug "#{rex.class} - #{req.inspect}"
229
+ debug rex.backtrace.join("\n")
230
+ end
164
231
  return rex.reply
165
- rescue RedirectException => rex
232
+ rescue OMF::SFA::Util::PromiseUnresolvedException => pex
233
+ require 'omf-sfa/am/am-rest/promise_handler' # delay loading as PromiseHandler sub classes this class
234
+ OMF::SFA::AM::Rest::PromiseHandler.register_promise(pex.promise,
235
+ pex.uuid,
236
+ req['_format'] == 'html',
237
+ Set.new((@coll_handlers || {}).keys))
238
+ #return [302, {'Location' => path}, ['Promised, but not ready yet.']]
239
+ rescue OMF::SFA::AM::Rest::RedirectException => rex
166
240
  debug "Redirecting to #{rex.path}"
167
- return [301, {'Location' => rex.path, "Content-Type" => ""}, ['Next window!']]
241
+ return [302, {'Location' => rex.path}, ['Next window, please.']]
242
+
168
243
  # rescue OMF::SFA::AM::AMManagerException => aex
169
244
  # return RackException.new(400, aex.to_s).reply
170
- rescue RetryLaterException => rex
171
- body = {
172
- type: 'retry',
173
- delay: rex.delay
174
- }
175
- debug "Retry later request"
176
- if req['_format'] == 'html'
177
- headers['Refresh'] = rex.delay.to_s
178
- opts = {} #{html_header: "<META HTTP-EQUIV='refresh' CONTENT='#{rex.delay}'>"}
179
- body = convert_to_html(body, env, opts)
180
- return [200 , headers, body + "\n"]
181
- end
182
- headers['Content-Type'] = 'application/json'
183
- return [504, headers, JSON.pretty_generate(body)]
245
+ # rescue RetryLaterException => rex
246
+ # body = {
247
+ # type: 'retry',
248
+ # delay: rex.delay,
249
+ # request_id: Thread.current[:request_context_id] || 'unknown'
250
+ # }
251
+ # debug "Retry later request - #{req.url}"
252
+ # if req['_format'] == 'html'
253
+ # refresh = rex.delay.to_s
254
+ # if (req_id = Thread.current[:request_context_id])
255
+ # refresh += "; url=#{req.url}&_request_id=#{req_id}"
256
+ # end
257
+ # headers['Refresh'] = refresh # 10; url=
258
+ # headers['X-Request-ID'] = req_id
259
+ # opts = {} #{html_header: "<META HTTP-EQUIV='refresh' CONTENT='#{rex.delay}'>"}
260
+ # body = convert_to_html(body, env, opts)
261
+ # return [200 , headers, body + "\n"]
262
+ # end
263
+ # headers['Content-Type'] = 'application/json'
264
+ # return [504, headers, JSON.pretty_generate(body)]
184
265
 
185
266
  rescue Exception => ex
186
267
  body = {
@@ -197,6 +278,37 @@ module OMF::SFA::AM::Rest
197
278
  end
198
279
  end
199
280
 
281
+ def _format_body(body, content_type, req, env, proxy_promise = nil)
282
+ begin
283
+ if req['_format'] == 'html'
284
+ #body = self.class.convert_to_html(body, env, Set.new((@coll_handlers || {}).keys))
285
+ content_type = 'text/html'
286
+ body = convert_to_html(body, env, {}, Set.new((@coll_handlers || {}).keys))
287
+ elsif content_type == 'application/json'
288
+ body = JSON.pretty_generate(body)
289
+ end
290
+ [content_type, body]
291
+ rescue OMF::SFA::Util::PromiseUnresolvedException => pex
292
+ proxy = OMF::SFA::Util::Promise.new
293
+ pex.promise.on_success do |d|
294
+ proxy.resolve [content_type, body]
295
+ end.on_error(proxy).on_progress(proxy)
296
+ raise OMF::SFA::Util::PromiseUnresolvedException.new proxy
297
+ end
298
+ end
299
+
300
+ # def _x(promise, req)
301
+ # uuid = 1 #pex.uuid
302
+ # path = "/promises/#{uuid}"
303
+ # require 'omf-sfa/am/am-rest/promise_handler' # delay loading as PromiseHandler sub classes this class
304
+ # OMF::SFA::AM::Rest::PromiseHandler.register_promise(promise,
305
+ # uuid,
306
+ # req['_format'] == 'html',
307
+ # Set.new((@coll_handlers || {}).keys))
308
+ # debug "Redirecting to #{path}"
309
+ # return [302, {'Location' => path}, ['Promised, but not ready yet.']]
310
+ # end
311
+
200
312
  def on_get(resource_uri, opts)
201
313
  debug 'get: resource_uri: "', resource_uri, '"'
202
314
  if resource_uri
@@ -207,6 +319,11 @@ module OMF::SFA::AM::Rest
207
319
  end
208
320
  end
209
321
 
322
+ def on_put(resource_uri, opts)
323
+ debug '>>> PUT NOT IMPLEMENTED'
324
+ raise UnsupportedMethodException.new('on_put', @resource_class)
325
+ end
326
+
210
327
  def on_post(resource_uri, opts)
211
328
  #debug 'POST: resource_uri "', resource_uri, '" - ', opts.inspect
212
329
  description, format = parse_body(opts, [:json, :form])
@@ -214,7 +331,7 @@ module OMF::SFA::AM::Rest
214
331
 
215
332
  if resource = opts[:resource]
216
333
  debug 'POST: Modify ', resource, ' --- ', resource.class
217
- modify_resource(resource, description, opts)
334
+ resource = modify_resource(resource, description, opts)
218
335
  else
219
336
  if description.is_a? Array
220
337
  resources = description.map do |d|
@@ -268,7 +385,7 @@ module OMF::SFA::AM::Rest
268
385
 
269
386
  def find_handler(path, opts)
270
387
  #debug "find_handler: path; '#{path}' opts: #{opts}"
271
- debug "find_handler: path; '#{path}'"
388
+ debug "find_handler: path: '#{path}'"
272
389
  rid = path.shift
273
390
  resource_id = opts[:resource_uri] = (rid ? URI.decode(rid) : nil) # make sure we get rid of any URI encoding
274
391
  opts[:resource] = nil
@@ -279,6 +396,7 @@ module OMF::SFA::AM::Rest
279
396
 
280
397
  raise OMF::SFA::AM::Rest::UnknownResourceException.new "Unknown resource '#{resource_id}'." unless resource
281
398
  opts[:context] = resource
399
+ opts[:contexts][opts[:context_name].to_sym] = resource
282
400
  comp = path.shift
283
401
  if (handler = @coll_handlers[comp.to_sym])
284
402
  opts[:context_name] = comp
@@ -292,7 +410,6 @@ module OMF::SFA::AM::Rest
292
410
  end
293
411
 
294
412
 
295
-
296
413
  protected
297
414
 
298
415
  def modify_resource(resource, description, opts)
@@ -301,6 +418,7 @@ module OMF::SFA::AM::Rest
301
418
  end
302
419
  description.delete(:href)
303
420
  resource.update(description) ? resource : nil
421
+ resource
304
422
  end
305
423
 
306
424
 
@@ -384,15 +502,34 @@ module OMF::SFA::AM::Rest
384
502
  #
385
503
  def populate_opts(req, opts)
386
504
  opts[:req] = req
505
+ opts[:context_name] = (req.env['REQUEST_PATH'].split('/') - req.path_info.split('/'))[-1]
506
+ opts[:contexts] ||= {}
387
507
  path = req.path_info.split('/').select { |p| !p.empty? }
388
508
  opts[:target] = find_handler(path, opts)
389
509
  rl = req.params.delete('_level')
390
- opts[:max_level] = rl ? rl.to_i : 0
510
+ opts[:max_level] = rl ? rl.to_i : 1
391
511
  #opts[:target].inspect
392
512
  opts
393
513
  end
394
514
 
515
+ # Return a named context resource. If it is a promise and not yet
516
+ # available, a TemporaryUnavailableException is being thrown.
517
+ #
518
+ # For instance if we have a path /users/xxx/friends/yyy/favorites
519
+ # by the time we get to the 'favorites' handler, we can access
520
+ # the respective 'users', 'friends' object through this method.
521
+ # PLEASE note the plurals 'users', 'friends'.
522
+ #
523
+ def get_context_resource(name, opts)
524
+ resource = opts[:contexts][name]
525
+ if resource.is_a? OMF::SFA::Util::Promise
526
+ resource = resource.value(OMF::SFA::AM::Rest::TemporaryUnavailableException)
527
+ end
528
+ resource
529
+ end
530
+
395
531
  def parse_body(opts, allowed_formats = [:json, :xml])
532
+ puts "ALLOWD>>>> #{allowed_formats}"
396
533
  req = opts[:req]
397
534
  body = req.body #req.POST
398
535
  raise EmptyBodyException.new unless body
@@ -424,12 +561,16 @@ module OMF::SFA::AM::Rest
424
561
  raise UnsupportedBodyFormatException.new(:json) unless allowed_formats.include?(:json)
425
562
  jb = JSON.parse(body)
426
563
  return [_rec_sym_keys(jb), :json]
564
+ when 'application/gjson'
565
+ raise UnsupportedBodyFormatException.new(:gjson) unless allowed_formats.include?(:gjson)
566
+ jb = JSON.parse(body)
567
+ return [_rec_sym_keys(jb), :gjson]
427
568
  when 'text/xml'
428
569
  xb = Nokogiri::XML(body)
429
570
  raise UnsupportedBodyFormatException.new(:xml) unless allowed_formats.include?(:xml)
430
571
  return [xb, :xml]
431
572
  when 'application/x-www-form-urlencoded'
432
- raise UnsupportedBodyFormatException.new(:xml) unless allowed_formats.include?(:form)
573
+ raise UnsupportedBodyFormatException.new(:form) unless allowed_formats.include?(:form)
433
574
  fb = req.POST
434
575
  #puts "FORM: #{fb.inspect}"
435
576
  return [fb, :form]
@@ -447,11 +588,18 @@ module OMF::SFA::AM::Rest
447
588
  def dispatch(req)
448
589
  opts = {}
449
590
  populate_opts(req, opts)
450
- opts[:req] = req
591
+ #opts[:req] = req
451
592
  #puts "OPTS>>>> #{opts.inspect}"
452
593
  method = req.request_method
453
594
  target = opts[:target] #|| self
595
+ if target.is_a? OMF::SFA::AM::Rest::ContentFoundException
596
+ raise target
597
+ end
454
598
  resource_uri = opts[:resource_uri]
599
+ _dispatch(method, target, resource_uri, opts)
600
+ end
601
+
602
+ def _dispatch(method, target, resource_uri, opts)
455
603
  case method
456
604
  when 'GET'
457
605
  res = target.on_get(resource_uri, opts)
@@ -468,22 +616,30 @@ module OMF::SFA::AM::Rest
468
616
 
469
617
  def show_resource_status(resource, opts)
470
618
  if resource
619
+ resource = resolve_promise(resource) do |r|
620
+ show_resource_status(r, opts)
621
+ end
471
622
  about = opts[:req].path
472
- props = resource.to_hash({}, :max_level => opts[:max_level])
623
+ refresh = ['', '1', 't', 'T', 'true', 'TRUE'].include?(opts[:req].params['_refresh'])
624
+ props = resource.to_hash({}, max_level: opts[:max_level], refresh: refresh)
473
625
  props.delete(:type)
474
- res = {
626
+ res = after_resource_to_hash_hook({
475
627
  #:about => about,
476
628
  :type => resource.resource_type,
477
- }.merge!(props)
629
+ }.merge!(props), resource)
478
630
  else
479
631
  res = {:error => 'Unknown resource'}
480
632
  end
481
-
482
- ['application/json', res]
633
+ check_for_promises 'application/json', res
483
634
  end
484
635
 
636
+ def after_resource_to_hash_hook(res_hash, resource)
637
+ res_hash
638
+ end
485
639
 
486
-
640
+ def absolute_path(rel_path)
641
+ "http://#{Thread.current[:http_host]}#{rel_path.start_with?('/') ? '' : '/'}#{rel_path}"
642
+ end
487
643
 
488
644
  def find_resource(resource_uri, description = {}, opts = {})
489
645
  descr = description.dup
@@ -497,10 +653,21 @@ module OMF::SFA::AM::Rest
497
653
  end
498
654
 
499
655
  #authenticator = Thread.current["authenticator"]
656
+ descr = _find_resource_before_hook(descr, opts)
657
+ _find_resource(descr)
658
+ end
659
+
660
+ # Allow sub class to override actual finding of resource - may create it on the fly
661
+ def _find_resource(descr)
500
662
  debug "Finding #{@resource_class}.first(#{descr})"
501
663
  @resource_class.first(descr)
502
664
  end
503
665
 
666
+ # Allow sub class to override search criteria for resource
667
+ def _find_resource_before_hook(descr, opts)
668
+ descr
669
+ end
670
+
504
671
  def show_resource_list(opts)
505
672
  # authenticator = Thread.current["authenticator"]
506
673
  if (context = opts[:context])
@@ -512,12 +679,21 @@ module OMF::SFA::AM::Rest
512
679
  end
513
680
 
514
681
  def show_resources(resources, resource_name, opts)
682
+ resources = resolve_promise(resources) do |r|
683
+ show_resources(r, resource_name, opts)
684
+ end
515
685
  #hopts = {max_level: opts[:max_level], level: 1}
516
- hopts = {max_level: opts[:max_level], level: 0}
686
+ hopts = {max_level: opts[:max_level] - 1, level: 0}
517
687
  objs = {}
518
688
  res_hash = resources.map do |a|
689
+ a = resolve_promise(a) do |r|
690
+ # The resolved promise was embeded, so we need to start afresh from the top
691
+ show_resources(resources, resource_name, opts)
692
+ end
519
693
  next unless a # TODO: This seems to be a bug in OProperty (removing objects)
520
- a.to_hash(objs, hopts)
694
+ #puts ">>>>>>>>> #{a.to_hash({}, hopts.dup)}"
695
+ #a.to_hash(objs, hopts.dup)
696
+ after_resource_to_hash_hook(a.to_hash(objs, hopts), a)
521
697
  #a.to_hash_brief(:href_use_class_prefix => true)
522
698
  end.compact
523
699
  if resource_name
@@ -529,9 +705,84 @@ module OMF::SFA::AM::Rest
529
705
  else
530
706
  res = res_hash
531
707
  end
532
- ['application/json', res]
708
+ check_for_promises 'application/json', res
533
709
  end
534
710
 
711
+ # Check if 'value' is a promise. If not, return it immediately. If it is
712
+ # a promise, return it's value if it has been already resolved. Otherwise
713
+ # throw a PromiseDeferException. If the promise gets resolved at some later
714
+ # stage the 'block' is called and is expected to return the same result as
715
+ # the caller of this function would have returned if this function would have
716
+ # returned immediately.
717
+ #
718
+ def resolve_promise(value, &block)
719
+ if (promise = value).is_a? OMF::SFA::Util::Promise
720
+ case promise.status
721
+ when :pending
722
+ proxy = OMF::SFA::Util::Promise.new
723
+ promise.on_success do |d|
724
+ proxy.resolve block.call(d)
725
+ end.on_error(proxy).on_progress(proxy)
726
+ raise OMF::SFA::Util::PromiseUnresolvedException.new proxy
727
+ when :rejected
728
+ raise promise.error_msg
729
+ else
730
+ value = promise.value
731
+ end
732
+ end
733
+ #puts "RESOLVE PROMISE(#{value.class}>>> #{value}"
734
+ value
735
+ end
736
+
737
+ # Check if any elements in res, which is either an array
738
+ # or hash, is a promise. If one is found, and can't be resolved,
739
+ # a PromiseDeferException is thrown. The proxy promise in the exception
740
+ # will monitor the unresolved promises and after all of them are resolved
741
+ # will resolve the associated promise with a 'clean' res.
742
+ #
743
+ def check_for_promises(mime_type, res, proxy = nil)
744
+ begin
745
+ res = _scan_for_promises(res)
746
+ rescue OMF::SFA::Util::PromiseUnresolvedException => pex
747
+ unless proxy
748
+ proxy = OMF::SFA::Util::Promise.new
749
+ pex.promise.on_success do |x|
750
+ check_for_promises(mime_type, res, proxy)
751
+ end.on_error do |ec, em|
752
+ proxy.reject(ec, em)
753
+ end.on_progress(proxy)
754
+ raise OMF::SFA::Util::PromiseUnresolvedException.new proxy
755
+ end
756
+ end
757
+ if proxy
758
+ proxy.resolve [mime_type, res]
759
+ end
760
+ [mime_type, res]
761
+ end
762
+
763
+ def _scan_for_promises(res)
764
+ if res.is_a? Array
765
+ res = res.map do |el|
766
+ _scan_for_promises(el)
767
+ end
768
+ elsif res.is_a? Hash
769
+ res.each do |key, val|
770
+ res[key] = _scan_for_promises(val)
771
+ end
772
+ elsif res.is_a? OMF::SFA::Util::Promise
773
+ case res.status
774
+ when :resolved
775
+ return res.value
776
+ when :rejected
777
+ raise res.err_message
778
+ else
779
+ raise OMF::SFA::Util::PromiseUnresolvedException.new(res)
780
+ end
781
+ end
782
+ res # seems to be 'normal' value
783
+ end
784
+
785
+
535
786
  def show_deleted_resource(uuid)
536
787
  res = {
537
788
  uuid: uuid,
@@ -602,12 +853,13 @@ module OMF::SFA::AM::Rest
602
853
  tmpl
603
854
  end
604
855
 
605
- def convert_to_html(body, env, opts, collections = Set.new)
856
+ def convert_to_html(obj, env, opts, collections = Set.new)
606
857
  req = ::Rack::Request.new(env)
607
858
  opts = {
608
859
  collections: collections,
609
860
  level: 0,
610
- href_prefix: "#{req.path}/"
861
+ href_prefix: "#{req.path}/",
862
+ env: env
611
863
  }.merge(opts)
612
864
 
613
865
  path = req.path.split('/').select { |p| !p.empty? }
@@ -617,11 +869,11 @@ module OMF::SFA::AM::Rest
617
869
  end
618
870
 
619
871
  res = []
620
- _convert_obj_to_html(body, nil, res, opts)
872
+ _convert_obj_to_html(obj, nil, res, opts)
621
873
 
622
874
  render_html(
623
875
  header: opts[:html_header] || '',
624
- result: body,
876
+ result: obj,
625
877
  title: @@service_name || env["HTTP_HOST"],
626
878
  service: h2.join('/'),
627
879
  content: res.join("\n")
@@ -636,6 +888,9 @@ module OMF::SFA::AM::Rest
636
888
  def _convert_obj_to_html(obj, ref_name, res, opts)
637
889
  klass = obj.class
638
890
  #puts "CONVERT>>>> #{ref_name} ... #{obj.class}::#{obj.to_s[0 .. 80]}"
891
+ if obj.is_a? OMF::SFA::Util::Promise
892
+ obj = obj.to_html()
893
+ end
639
894
  if (obj.is_a? OMF::SFA::Resource::OPropertyArray) || obj.is_a?(Array)
640
895
  if obj.empty?
641
896
  res << '<span class="empty">empty</span>'
@@ -661,7 +916,7 @@ module OMF::SFA::AM::Rest
661
916
  end
662
917
 
663
918
  def _convert_array_to_html(array, ref_name, res, opts)
664
- opts = opts.merge(level: opts[:level] + 1)
919
+ opts = opts.merge(level: opts[:level] + 1, context: array)
665
920
  array.each do |obj|
666
921
  #puts "AAA>>>> #{obj}::#{opts}"
667
922
  name = nil
@@ -684,6 +939,7 @@ module OMF::SFA::AM::Rest
684
939
 
685
940
  def _convert_hash_to_html(hash, ref_name, res, opts)
686
941
  #puts ">>>> #{hash}::#{opts}"
942
+ opts = opts.merge(context: hash)
687
943
  hash.each do |key, obj|
688
944
  #key = "#{key}-#{opts[:level]}-#{opts[:collections].to_a.inspect}"
689
945
  if opts[:level] == 0 && opts[:collections].include?(key.to_sym)