tofu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tofu.rb ADDED
@@ -0,0 +1,636 @@
1
+ require "tofu/version"
2
+
3
+ require 'erb'
4
+ require 'drb/drb'
5
+ require 'monitor'
6
+ require 'digest/md5'
7
+ require 'webrick'
8
+ require 'webrick/cgi'
9
+ require 'uri'
10
+
11
+ module Tofu
12
+ class Session
13
+ include MonitorMixin
14
+
15
+ def initialize(bartender, hint=nil)
16
+ super()
17
+ @session_id = Digest::MD5.hexdigest(Time.now.to_s + __id__.to_s)
18
+ @contents = {}
19
+ @hint = hint
20
+ renew
21
+ end
22
+ attr_reader :session_id
23
+ attr_accessor :hint
24
+
25
+ def service(context)
26
+ case context.req_method
27
+ when 'GET', 'POST', 'HEAD'
28
+ do_GET(context)
29
+ else
30
+ context.res_method_not_allowed
31
+ end
32
+ end
33
+
34
+ def expires
35
+ Time.now + 24 * 60 * 60
36
+ end
37
+
38
+ def hint_expires
39
+ Time.now + 60 * 24 * 60 * 60
40
+ end
41
+
42
+ def renew
43
+ @expires = expires
44
+ end
45
+
46
+ def expired?
47
+ @expires && Time.now > @expires
48
+ end
49
+
50
+ def do_GET(context)
51
+ dispatch_event(context)
52
+ tofu = lookup_view(context)
53
+ body = tofu ? tofu.to_html(context) : ''
54
+ context.res_header('content-type', 'text/html; charset=utf-8')
55
+ context.res_body(body)
56
+ end
57
+
58
+ def dispatch_event(context)
59
+ params = context.req_params
60
+ tofu_id ,= params['tofu_id']
61
+ tofu = fetch(tofu_id)
62
+ return unless tofu
63
+ tofu.send_request(context, context.req_params)
64
+ end
65
+
66
+ def lookup_view(context)
67
+ nil
68
+ end
69
+
70
+ def entry(tofu)
71
+ synchronize do
72
+ @contents[tofu.tofu_id] = tofu
73
+ end
74
+ end
75
+
76
+ def fetch(ref)
77
+ @contents[ref]
78
+ end
79
+ end
80
+
81
+ class SessionBar
82
+ include MonitorMixin
83
+
84
+ def initialize
85
+ super()
86
+ @pool = {}
87
+ @keeper = keeper
88
+ @interval = 60
89
+ end
90
+
91
+ def store(session)
92
+ key = session.session_id
93
+ synchronize do
94
+ @pool[key] = session
95
+ end
96
+ @keeper.wakeup
97
+ return key
98
+ end
99
+
100
+ def fetch(key)
101
+ return nil if key.nil?
102
+ synchronize do
103
+ session = @pool[key]
104
+ return nil unless session
105
+ if session.expired?
106
+ @pool.delete(key)
107
+ return nil
108
+ end
109
+ return session
110
+ end
111
+ end
112
+
113
+ private
114
+ def keeper
115
+ Thread.new do
116
+ loop do
117
+ synchronize do
118
+ @pool.delete_if do |k, v|
119
+ v.nil? || v.expired?
120
+ end
121
+ end
122
+ Thread.stop if @pool.size == 0
123
+ sleep @interval
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ class Bartender
130
+ def initialize(factory, name=nil)
131
+ @factory = factory
132
+ @prefix = name ? name : factory.to_s.split(':')[-1]
133
+ @bar = SessionBar.new
134
+ end
135
+ attr_reader :prefix
136
+
137
+ def service(context)
138
+ begin
139
+ session = retrieve_session(context)
140
+ catch(:tofu_done) { session.service(context) }
141
+ store_session(context, session)
142
+ ensure
143
+ end
144
+ end
145
+
146
+ private
147
+ def retrieve_session(context)
148
+ sid = context.req_cookie(@prefix + '_id')
149
+ session = @bar.fetch(sid) || make_session(context)
150
+ return session
151
+ end
152
+
153
+ def store_session(context, session)
154
+ sid = @bar.store(session)
155
+ context.res_add_cookie(@prefix + '_id', sid, session.expires)
156
+ hint = session.hint
157
+ if hint
158
+ context.res_add_cookie(@prefix +'_hint', hint, session.hint_expires)
159
+ end
160
+ session.renew
161
+ return sid
162
+ end
163
+
164
+ def make_session(context)
165
+ hint = context.req_cookie(@prefix + '_hint')
166
+ @factory.new(self, hint)
167
+ end
168
+ end
169
+
170
+ class ERBMethod
171
+ def initialize(method_name, fname, dir=nil)
172
+ @fname = build_fname(fname, dir)
173
+ @method_name = method_name
174
+ end
175
+
176
+ def reload(mod)
177
+ erb = File.open(@fname) {|f| ERB.new(f.read)}
178
+ erb.def_method(mod, @method_name, @fname)
179
+ end
180
+
181
+ private
182
+ def build_fname(fname, dir)
183
+ case dir
184
+ when String
185
+ ary = [dir]
186
+ when Array
187
+ ary = dir
188
+ else
189
+ ary = $:
190
+ end
191
+
192
+ found = fname # default
193
+ ary.each do |dir|
194
+ path = File::join(dir, fname)
195
+ if File::readable?(path)
196
+ found = path
197
+ break
198
+ end
199
+ end
200
+ found
201
+ end
202
+ end
203
+
204
+ class Tofu
205
+ include DRbUndumped
206
+ include ERB::Util
207
+
208
+ @erb_method = []
209
+ def self.add_erb(method_name, fname, dir=nil)
210
+ erb = ERBMethod.new(method_name, fname, dir)
211
+ @erb_method.push(erb)
212
+ end
213
+
214
+ def self.set_erb(fname, dir=nil)
215
+ @erb_method = [ERBMethod.new('to_html(context=nil)', fname, dir)]
216
+ reload_erb
217
+ end
218
+
219
+ def self.reload_erb1(erb)
220
+ erb.reload(self)
221
+ rescue SyntaxError
222
+ end
223
+
224
+ def self.reload_erb
225
+ @erb_method.each do |erb|
226
+ reload_erb1(erb)
227
+ end
228
+ end
229
+
230
+ def initialize(session)
231
+ @session = session
232
+ @session.entry(self)
233
+ @tofu_seq = nil
234
+ end
235
+ attr_reader :session
236
+
237
+ def tofu_class
238
+ self.class.to_s
239
+ end
240
+
241
+ def tofu_id
242
+ self.__id__.to_s
243
+ end
244
+
245
+ def to_div(context)
246
+ to_elem('div', context)
247
+ end
248
+
249
+ def to_span(context)
250
+ to_elem('span', context)
251
+ end
252
+
253
+ def to_elem(elem, context)
254
+ elem('elem', {'class'=>tofu_class, 'id'=>tofu_id}) {
255
+ begin
256
+ to_html(context)
257
+ rescue
258
+ "<p>error! #{h($!)}</p>"
259
+ end
260
+ }
261
+ end
262
+
263
+ def to_html(context); ''; end
264
+
265
+ def to_inner_html(context)
266
+ to_html(context)
267
+ end
268
+
269
+ def send_request(context, params)
270
+ cmd, = params['tofu_cmd']
271
+ msg = 'do_' + cmd.to_s
272
+
273
+ if @tofu_seq
274
+ seq, = params['tofu_seq']
275
+ unless @tofu_seq.to_s == seq
276
+ p [seq, @tofu_seq.to_s] if $DEBUG
277
+ return
278
+ end
279
+ end
280
+
281
+ if respond_to?(msg)
282
+ send(msg, context, params)
283
+ else
284
+ do_else(context, params)
285
+ end
286
+ ensure
287
+ @tofu_seq = @tofu_seq.succ if @tofu_seq
288
+ end
289
+
290
+ def do_else(context, params)
291
+ end
292
+
293
+ def action(context)
294
+ context.req_script_name.to_s + context.req_path_info.to_s
295
+ end
296
+
297
+ private
298
+ def attr(opt)
299
+ ary = opt.collect do |k, v|
300
+ if v
301
+ %Q!#{k}="#{h(v)}"!
302
+ else
303
+ nil
304
+ end
305
+ end.compact
306
+ return nil if ary.size == 0
307
+ ary.join(' ')
308
+ end
309
+
310
+ def elem(name, opt={})
311
+ head = ["#{name}", attr(opt)].compact.join(" ")
312
+ if block_given?
313
+ %Q!<#{head}>\n#{yield}\n</#{name}>!
314
+ else
315
+ %Q!<#{head} />!
316
+ end
317
+ end
318
+
319
+ def make_param(method_name, add_param={})
320
+ param = {
321
+ 'tofu_id' => tofu_id,
322
+ 'tofu_cmd' => method_name
323
+ }
324
+ param['tofu_seq'] = @tofu_seq if @tofu_seq
325
+ param.update(add_param)
326
+ return param
327
+ end
328
+
329
+ def form(method_name, context_or_param, context_or_empty=nil)
330
+ if context_or_empty.nil?
331
+ context = context_or_param
332
+ add_param = {}
333
+ else
334
+ context = context_or_empty
335
+ add_param = context_or_param
336
+ end
337
+ param = make_param(method_name, add_param)
338
+ hidden = input_hidden(param)
339
+ %Q!<form action="#{action(context)}" method="post">\n! + hidden
340
+ end
341
+
342
+ def href(method_name, add_param, context)
343
+ param = make_param(method_name, add_param)
344
+ ary = param.collect do |k, v|
345
+ "#{u(k)}=#{u(v)}"
346
+ end
347
+ %Q!href="#{action(context)}?#{ary.join(';')}"!
348
+ end
349
+
350
+ def input_hidden(param)
351
+ ary = param.collect do |k, v|
352
+ %Q!<input type="hidden" name="#{h(k)}" value="#{h(v)}" />\n!
353
+ end
354
+ ary.join('')
355
+ end
356
+
357
+ def make_anchor(method, param, context)
358
+ "<a #{href(method, param, context)}>"
359
+ end
360
+
361
+ def a(method, param, context)
362
+ make_anchor(method, param, context)
363
+ end
364
+ end
365
+
366
+ def reload_erb
367
+ ObjectSpace.each_object(Class) do |o|
368
+ if o.ancestors.include?(Tofu::Tofu)
369
+ o.reload_erb
370
+ end
371
+ end
372
+ end
373
+ module_function :reload_erb
374
+
375
+ class Context
376
+ def initialize(req, res)
377
+ @req = req
378
+ @res = res
379
+ end
380
+ attr_reader :req, :res
381
+
382
+ def done
383
+ throw(:tofu_done)
384
+ rescue NameError
385
+ nil
386
+ end
387
+
388
+ def service(bartender)
389
+ bartender.service(self)
390
+ nil
391
+ end
392
+
393
+ def req_params
394
+ hash = {}
395
+ @req.query.each do |k,v|
396
+ hash[k] = v.list
397
+ end
398
+ hash
399
+ end
400
+
401
+ def req_cookie(name)
402
+ found = @req.cookies.find {|c| c.name == name}
403
+ found ? found.value : nil
404
+ end
405
+
406
+ def res_add_cookie(name, value, expires=nil)
407
+ c = WEBrick::Cookie.new(name, value)
408
+ c.expires = expires if expires
409
+ @res.cookies.push(c)
410
+ end
411
+
412
+ def req_method
413
+ @req.request_method
414
+ end
415
+
416
+ def res_method_not_allowed
417
+ raise HTTPStatus::MethodNotAllowed, "unsupported method `#{req_method}'."
418
+ end
419
+
420
+ def req_path_info
421
+ @req.path_info
422
+ end
423
+
424
+ def req_script_name
425
+ @req.script_name
426
+ end
427
+
428
+ def req_absolute_path
429
+ (@req.request_uri + '/').to_s.chomp('/')
430
+ end
431
+
432
+ def res_body(v)
433
+ @res.body = v
434
+ end
435
+
436
+ def res_header(k, v)
437
+ if k.downcase == 'status'
438
+ @res.status = v.to_i
439
+ return
440
+ end
441
+ @res[k] = v
442
+ end
443
+ end
444
+
445
+ class Tofulet < WEBrick::HTTPServlet::AbstractServlet
446
+ def initialize(config, bartender, *options)
447
+ @bartender = bartender
448
+ super(config, *options)
449
+ @logger.debug("#{self.class}(initialize)")
450
+ end
451
+ attr_reader :logger, :config, :options, :bartender
452
+
453
+ def service(req, res)
454
+ Context.new(req, res).service(@bartender)
455
+ end
456
+ end
457
+
458
+ class CGITofulet < WEBrick::CGI
459
+ def initialize(bartender, *args)
460
+ @bartender = bartender
461
+ super(*args)
462
+ end
463
+
464
+ def service(req, res)
465
+ Context.new(req, res).service(@bartender)
466
+ end
467
+ end
468
+ end
469
+
470
+ module Tofu
471
+ class Tofu
472
+ def update_js
473
+ <<-"EOS"
474
+ function tofu_x_eval(tofu_id) {
475
+ var ary = document.getElementsByName(tofu_id + "tofu_x_eval");
476
+ for (var j = 0; j < ary.length; j++) {
477
+ var tofu_arg = ary[j];
478
+ for (var i = 0; i < tofu_arg.childNodes.length; i++) {
479
+ var node = tofu_arg.childNodes[i];
480
+ if (node.attributes.getNamedItem('name').nodeValue == 'tofu_x_eval') {
481
+ var script = node.attributes.getNamedItem('value').nodeValue;
482
+ try {
483
+ eval(script);
484
+ } catch(e) {
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+
491
+ function tofu_x_update(tofu_id, url) {
492
+ var x;
493
+ try {
494
+ x = new ActiveXObject("Msxml2.XMLHTTP");
495
+ } catch (e) {
496
+ try {
497
+ x = new ActiveXObject("Microsoft.XMLHTTP");
498
+ } catch (e) {
499
+ x = null;
500
+ }
501
+ }
502
+ if (!x && typeof XMLHttpRequest != "undefined") {
503
+ x = new XMLHttpRequest();
504
+ }
505
+ if (x) {
506
+ x.onreadystatechange = function() {
507
+ if (x.readyState == 4 && x.status == 200) {
508
+ var tofu = document.getElementById(tofu_id);
509
+ tofu.innerHTML = x.responseText;
510
+ tofu_x_eval(tofu_id);
511
+ }
512
+ }
513
+ x.open("GET", url);
514
+ x.send(null);
515
+ }
516
+ }
517
+ EOS
518
+ end
519
+
520
+ def a_and_update(method_name, add_param, context, target=nil)
521
+ target ||= self
522
+ param = {
523
+ 'tofu_inner_id' => target.tofu_id
524
+ }
525
+ param.update(add_param)
526
+
527
+ param = make_param(method_name, param)
528
+ ary = param.collect do |k, v|
529
+ "#{u(k)}=#{u(v)}"
530
+ end
531
+ path = URI.parse(context.req_absolute_path)
532
+ url = path + %Q!#{action(context)}?#{ary.join(';')}!
533
+ %Q!tofu_x_update("#{target.tofu_id}", #{url.to_s.dump});!
534
+ end
535
+
536
+ def on_update_script(ary_or_script)
537
+ ary = if String === ary_or_script
538
+ [ary_or_script]
539
+ else
540
+ ary_or_script
541
+ end
542
+ str = %Q!<form name="#{tofu_id}tofu_x_eval">!
543
+ ary.each do |script|
544
+ str << %Q!<input type='hidden' name='tofu_x_eval' value="#{script.gsub('"', '&quot;')}" />!
545
+ end
546
+ str << '</form>'
547
+ str
548
+ end
549
+
550
+ def update_me(context)
551
+ a_and_update('else', {}, context)
552
+ end
553
+
554
+ def update_after(msec, context)
555
+ callback = update_me(context)
556
+ script = %Q!setTimeout(#{callback.dump}, #{msec})!
557
+ on_update_script(script)
558
+ end
559
+ end
560
+
561
+ class Session
562
+ def do_inner_html(context)
563
+ params = context.req_params
564
+ tofu_id ,= params['tofu_inner_id']
565
+ return false unless tofu_id
566
+
567
+ tofu = fetch(tofu_id)
568
+ body = tofu ? tofu.to_inner_html(context) : ''
569
+
570
+ context.res_header('content-type', 'text/html; charset=utf-8')
571
+ context.res_body(body)
572
+
573
+ throw(:tofu_done)
574
+ end
575
+ end
576
+ end
577
+
578
+ if __FILE__ == $0
579
+ require 'pp'
580
+
581
+ class EnterTofu < Tofu::Tofu
582
+ ERB.new(<<EOS).def_method(self, 'to_html(context)')
583
+ <%=form('enter', {}, context)%>
584
+ <dl>
585
+ <dt>hint</dt><dd><%=h @session.hint %><input class='enter' type='text' size='40' name='hint' value='<%=h @session.hint %>'/></dd>
586
+ <dt>volatile</dt><dd><%=h @session.text %><input class='enter' type='text' size='40' name='text' value='<%=h @session.text%>'/></dd>
587
+ </dl>
588
+ <input type='submit' />
589
+ </form>
590
+ EOS
591
+ def do_enter(context, params)
592
+ hint ,= params['hint']
593
+ @session.hint = hint || ''
594
+ text ,= params['text']
595
+ @session.text = text || ''
596
+ end
597
+ end
598
+
599
+ class BaseTofu < Tofu::Tofu
600
+ ERB.new(<<EOS).def_method(self, 'to_html(context)')
601
+ <html><title>base</title><body>
602
+ Hello, World.
603
+ <%= @enter.to_html(context) %>
604
+ <hr />
605
+ <pre><%=h context.pretty_inspect%></pre>
606
+ </body></html>
607
+ EOS
608
+ def initialize(session)
609
+ super(session)
610
+ @enter = EnterTofu.new(session)
611
+ end
612
+ end
613
+
614
+ class HelloSession < Tofu::Session
615
+ def initialize(bartender, hint=nil)
616
+ super
617
+ @base = BaseTofu.new(self)
618
+ @text = ''
619
+ end
620
+ attr_accessor :text
621
+
622
+ def lookup_view(context)
623
+ @base
624
+ end
625
+ end
626
+
627
+ tofu = Tofu::Bartender.new(HelloSession)
628
+ if false
629
+ DRb.start_service('druby://localhost:54322', Tofu::CGITofulet.new(tofu))
630
+ DRb.thread.join
631
+ else
632
+ s = WEBrick::HTTPServer.new(:Port => 8080)
633
+ s.mount("/", Tofu::Tofulet, tofu)
634
+ s.start
635
+ end
636
+ end
@@ -0,0 +1,53 @@
1
+ <html>
2
+ <head>
3
+ <script language="javascript">
4
+ <%= update_js %>
5
+ </script>
6
+ <title><%=h @session.headline %></title>
7
+ <style type="text/css" media="screen">
8
+ body {
9
+ font-family: Helvetica;
10
+ background: #FFFFFF;
11
+ color: #000000;
12
+ }
13
+
14
+ .EnterTofu * input {
15
+ box-sizing: border-box;
16
+ width: 100%;
17
+ padding: 16px 6px 6px 6px;
18
+ font-size: 16px;
19
+ font-weight: normal;
20
+ background: #6d84a2;
21
+ }
22
+
23
+ .ListTofu > ul {
24
+ margin: 0;
25
+ padding: 0;
26
+ }
27
+
28
+ .ListTofu > ul > li {
29
+ border-bottom: 1px solid #E0E0E0;
30
+ padding: 8px 0 8px 10px;
31
+ font-size: 14px;
32
+ font-weight: bold;
33
+ list-style: none:
34
+ }
35
+
36
+ .ListTofu > ul > li.group {
37
+ top: -1px;
38
+ margin-bottom: -2px;
39
+ border-top: 1px solid #7d7d7d;
40
+ border-bottom: 1px solid #999999;
41
+ padding: 1px 5px;
42
+ font-size: 13px;
43
+ font-weight: bold;
44
+ text-shadow: rgba(0, 0, 0, 0.4) 0 1px 0;
45
+ color: #FFFFFF;
46
+ }
47
+ </style>
48
+ </head>
49
+ <body onload='setTimeout(<%= @list.update_me(context).dump%>, <%= @session.interval %>)'>
50
+ <%= @enter.to_div(context)%>
51
+ <%= @list.to_div(context)%>
52
+ </body>
53
+ </html>
@@ -0,0 +1,3 @@
1
+ <%=form('enter', {}, context)%>
2
+ <input class='enter' type='text' size='40' name='str' value='' autocomplete='off' autofocus/>
3
+ </form>