1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
|
#!/usr/bin/env ruby
# a direct request is just passed on
#
# exaclient --direct --request=somerequest.exa --result=somefile.pdf
#
# in an extended request the filename in the template file is replaced by the filename
# given on the command line; templates are located on the current path and at parent
# directories (two levels); the filename is expanded to a full path
#
# exaclient --extend --template=tmicare-l-h.exa --file=somefile.xml --result=somefile.pdf
#
# a constructed request is build out of the provided filename and action; the filename is
# expanded to a full path
#
# exaclient --construct --action=tmicare-s-h.exa --file=somefile.xml --result=somefile.pdf
#
# in all cases, the result is either determined by a switch or taken from a reply file
banner = ['WWWClient', 'version 1.0.0', '2003-2006', 'PRAGMA ADE/POD']
$: << File.dirname(File.expand_path($0))
require 'base/switch'
require 'base/logger'
require 'timeout'
require 'thread'
require 'rexml/document'
require 'net/http'
class File
def File.backtracked(filename,level=3)
if level > 0 && filename && ! filename.empty? then
if FileTest.file?(filename) then
filename
else
File.backtracked('../'+filename,level-1)
end
else
filename
end
end
def File.expanded(filename)
File.expand_path(filename)
end
end
class Commands
include CommandBase
end
class Commands
@@namespace = "xmlns:exa='http://www.pragma-ade.com/schemas/example.rng'"
@@randchars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "abcdefghijklmnopqrstuvwxyz"
def traceback
"(error: #{$!})" + "\n -- " + $@.join("\n >>")
end
def pdf(action,filename,enabled)
if enabled && FileTest.file?(filename) then
begin
report("pdf action #{action} on #{filename}")
case action
when 'close' then system("pdfclose --all")
when 'open' then system("pdfopen --file #{filename}")
end
rescue
# forget about it
end
end
end
def status(replyfile,str) # when block, then ok
begin
# def status(*whatever)
# end
File.open(replyfile,'w') do |f|
report("saving reply info in '#{replyfile}'")
f.puts("<?xml version='1.0'?>\n\n")
f.puts("<exa:reply #{@@namespace}>\n")
if block_given? then
f.puts(" <exa:status>ok</exa:status>\n")
f.puts(" #{yield}\n")
else
f.puts(" <exa:status>error</exa:status>\n")
end
f.puts(" <exa:comment>" + str + "</exa:comment>\n")
f.puts("</exa:reply>\n")
f.close
report("saving status: #{str}")
end
rescue
report("saving reply info in '#{replyfile}' fails")
ensure
exit
end
exit # to be real sure
end
def boundary_string (length) # copied from webrick/utils
rand_max = @@randchars.size
ret = ""
length.times do
ret << @@randchars[rand(rand_max)]
end
ret.upcase
end
end
class Commands
@@connecttimeout = 10*60 # ten minutes
@@processtimeout = 60*60 # an hour
@@polldelay = 5 # 5 seconds
def main
datatemplate = @commandline.option('template')
datafile = @commandline.option('file')
dataaction = @commandline.option('action')
if ! datatemplate.empty? then
report("template '#{datatemplate}' specified without --construct")
report("aborting")
elsif ! dataaction.empty? then
report("action data '#{dataaction}' specified without --construct or --extend")
report("aborting")
elsif ! datafile.empty? then
report("action file '#{datafile}' specified without --construct or --extend")
report("aborting")
else
report("assuming --direct")
direct()
end
end
def construct
requestfile = @commandline.option('request')
replyfile = @commandline.option('reply')
datatemplate = @commandline.option('template')
datafile = @commandline.option('file')
dataaction = @commandline.option('action')
domain = @commandline.option('domain')
project = @commandline.option('project')
username = @commandline.option('username')
password = @commandline.option('password')
threshold = @commandline.option('threshold')
datablob = ''
begin
datablob = IO.read(datatemplate)
rescue
datablob = ''
else
begin
request = REXML::Document.new(datablob)
if e = REXML::XPath.match(request.root,"/exa:request/exa:data") then
datablob = e.to_s.chomp
end
rescue
datablob = ''
end
end
begin
File.open(requestfile,'w') do |f|
f.puts "<?xml version='1.0'?>\n"
f.puts "<exa:request #{@@namespace}>\n"
f.puts " <exa:application>\n"
f.puts " <exa:action>#{dataaction}</exa:action>\n" unless dataaction.empty?
f.puts " <exa:filename>#{datafile}</exa:filename>\n" unless datafile.empty?
f.puts " <exa:threshold>#{threshold}</exa:threshold>\n" unless threshold.empty?
f.puts " </exa:application>\n"
f.puts " <exa:client>\n"
f.puts " <exa:domain>#{domain}</exa:domain>\n"
f.puts " <exa:project>#{project}</exa:project>\n"
f.puts " <exa:username>#{username}</exa:username>\n"
f.puts " <exa:password>#{password}</exa:password>\n"
f.puts " </exa:client>\n"
if datablob.empty? then
f.puts " <exa:data/>\n"
else
f.puts " #{datablob.chomp}\n"
end
f.puts "</exa:request>"
end
rescue
status(replyfile,"unable to create '#{requestfile}'")
end
direct()
end
def extend
requestfile = @commandline.option('request')
replyfile = @commandline.option('reply')
datatemplate = @commandline.option('template')
datafile = @commandline.option('file')
dataaction = @commandline.option('action')
threshold = @commandline.option('threshold')
if datatemplate.empty? then
status(replyfile,"invalid data template '#{datatemplate}'")
else
begin
if FileTest.file?(datatemplate) && oldrequest = IO.read(datatemplate) then
request, done = REXML::Document.new(oldrequest), false
if ! threshold.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:threshold") then
e.text, done = threshold, true
end
if ! dataaction.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:action") then
e.text, done = dataaction, true
end
if ! datafile.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:filename") then
e.text, done = datafile, true
end
#
if ! threshold.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application") then
e = e.add_element('exa:threshold')
e.add_text(threshold.to_s)
done = true
end
#
report("nothing replaced in template file") unless done
begin
File.open(requestfile,'w') do |f|
f.puts(newrequest.to_s)
end
rescue
status(replyfile,"unable to create '#{requestfile}'")
end
else
status(replyfile,"unable to read data template '#{datatemplate}'")
end
rescue
status(replyfile,"unable to handle data template '#{datatemplate}'")
end
end
direct()
end
def direct
requestpath = @commandline.option('path')
requestfile = @commandline.option('request')
replyfile = @commandline.option('reply')
resultfile = @commandline.option('result')
datatemplate = @commandline.option('template')
datafile = @commandline.option('file')
threshold = @commandline.option('threshold')
address = @commandline.option('address')
port = @commandline.option('port')
session_id = @commandline.option('session')
exaurl = @commandline.option('exaurl')
exaurl = "/#{exaurl}" unless exaurl =~ /^\//
address.sub!(/^http\:\/\//io) do
''
end
address.sub!(/\:(\d+)$/io) do
port = $1
''
end
autopdf = @commandline.option('autopdf')
dialogue = nil
resultfile.sub!(/\.[a-z]+?$/, '') # don't overwrite the source
unless requestpath.empty? then
begin
if FileTest.directory?(requestpath) then
if Dir.chdir(requestpath) then
report("gone to path '#{requestpath}'")
else
status(replyfile,"unable to go to path '#{requestpath}")
end
else
status(replyfile,"unable to locate '#{requestpath}'")
end
rescue
status(replyfile,"unable to handle '#{requestpath}'")
end
end
datafile = File.expand_path(datafile) unless datafile.empty?
datatemplate = File.backtracked(datatemplate,3) unless datatemplate.empty?
# request must be valid
status(replyfile,'no request file') if requestfile.empty?
status(replyfile,"invalid request file '#{requestfile}'") unless FileTest.file?(requestfile)
begin
request = IO.readlines(requestfile).join('')
request = REXML::Document.new(request)
status(replyfile,'invalid request (no request)') unless request.root.fully_expanded_name=='exa:request'
status(replyfile,'invalid request (no application block)') unless request.elements['exa:request'].elements['exa.application'] == nil # explicit nil test needed
rescue REXML::ParseException
status(replyfile,'invalid request (invalid xml file)')
rescue
status(replyfile,'invalid request (invalid file)')
else
report("using request file '#{requestfile}'")
end
# request can force session_id
if session_id && session_id.empty? then
begin
id = request.elements['exa:request'].elements['exa:application'].elements['exa:session'].text
rescue Exception
id = ''
ensure
if id && ! id.empty? then
session_id = id
end
end
end
# request can overload reply name
begin
rreplyfile = request.elements['exa:request'].elements['exa:application'].elements['exa:output'].text
rescue Exception
rreplyfile = nil
ensure
if rreplyfile && ! rreplyfile.empty? then
replyfile = rreplyfile
report("reply file '#{replyfile} set by request'")
else
report("using reply file '#{replyfile}'")
end
end
# request can overload result name
begin
rresultfile = request.elements['exa:request'].elements['exa:application'].elements['exa:result']
rescue Exception
rresultfile = nil
ensure
if rresultfile && ! rresultfile.empty? then
resultfile = rresultfile
report("result file '#{resultfile}' set by request")
else
report("using result file '#{resultfile}'")
end
end
# try to connect to server
start_time = Time.now
processtimeout = begin @commandline.option('timeout').to_i rescue @@processtimeout end
processtimeout = @@processtimeout if processtimeout == 0 # 'xx'.to_i => 0
dialogue = start_dialogue(address, port, processtimeout)
if dialogue then
# continue
else
status(replyfile,'no connection')
end
# post request
timeout (@@processtimeout-10) do # -10 so that we run into this one first
begin
report("posting request of type '#{exaurl}'")
report("using session id '#{session_id}'") if session_id && ! session_id.empty?
firstline, chunks, total = nil, 0, 0
body, boundary, crlf = '', boundary_string(32), "\x0d\x0a"
body << '--' + boundary + crlf
body << "Content-Disposition: form-data; name=\"exa:request\""
body << crlf
body << "Content-Type: text/plain"
body << crlf + crlf
body << request.to_s
body << crlf + '--' + boundary + crlf
if session_id && ! session_id.empty? then
body << "Content-Disposition: form-data; name=\"exa:session\""
body << "Content-Type: text/plain"
body << crlf + crlf
body << session_id
body << crlf + '--' + boundary + crlf
end
begin
File.open(datafile,'rb') do |df|
body << "Content-Disposition: form-data; name=\"filename\""
body << "Content-Type: text/plain"
body << crlf + crlf
body << datafile
body << crlf + '--' + boundary + crlf
body << "Content-Disposition: form-data; name=\"fakename\" ; filename=\"#{datafile}\""
body << "Content-Type: application/octetstream"
body << "Content-Transfer-Encoding: binary"
body << crlf + crlf
body << df.read
body << crlf + '--' + boundary + '--' + crlf
end
rescue
# skip
end
headers = Hash.new
headers['content-type'] = "multipart/form-data; boundary=#{boundary}"
headers['content-length'] = body.length.to_s
begin
File.open(resultfile,'wb') do |rf|
begin
# firstline is max 1024 but ok for reply
dialogue.post(exaurl,body,headers) do |str|
if ! firstline || firstline.empty? then
report('receiving result') if total == 0
firstline = str
end
total += 1
rf.write(str)
end
rescue
report("forced close #{traceback}")
end
end
rescue
status(replyfile,'cannot open file')
end
begin
File.delete(resultfile) if File.zero?(resultfile)
rescue
end
unless FileTest.file?(resultfile) then
report("deleting empty resultfile")
begin
File.delete(resultfile)
rescue
# nice try, an error anyway
end
status(replyfile,'empty file')
else
n, id, status = 0, '', ''
loop do
again = false
if ! dialogue then
again = true
elsif firstline =~ /(\<exa:reply)/moi then
begin
reply = REXML::Document.new(firstline)
id = (REXML::XPath.match(reply.root,"/exa:reply/exa:session/text()") || '').to_s
status = (REXML::XPath.match(reply.root,"/exa:reply/exa:status/text()") || '').to_s
rescue
report("error in parsing reply #{traceback}")
break
else
report("status: #{status}")
if (status =~ /^running\s*\:\s*(background|busy)$/i) && (! id.empty?) then
report("waiting for status reply (#{n*@@polldelay})")
again = true
end
end
end
if again then
n += 1
sleep(@@polldelay) # todo: duplicate when n > 1
unless dialogue then
report('reestablishing connection')
dialogue = start_dialogue(address, port, processtimeout)
end
if dialogue then
begin
File.open(resultfile,'wb') do |rf|
begin
body = "id=#{id}"
headers = Hash.new
headers['content-type'] = "application/x-www-form-urlencoded"
headers['content-length'] = body.length.to_s
total, firstline = 0, ''
dialogue.post("/exastatus",body,headers) do |str|
if ! firstline || firstline.empty? then
firstline = str
end
total += 1
rf.write(str)
end
rescue
report("forced close #{traceback}")
dialogue = nil
again = true
end
end
begin
File.delete(resultfile) if File.zero?(resultfile)
rescue
end
rescue
report("error in opening file #{traceback}")
status(replyfile,'cannot open file')
end
else
report("unable to make a connection")
status(replyfile,'unable to make a connection') # exit
end
else
break
end
end
case firstline
when /<\?xml\s*version=.*?\?>\s*<exa:reply/moi then
begin
File.delete(replyfile) if FileTest.file?(replyfile)
resultfile = replyfile if File.rename(resultfile,replyfile)
rescue
end
report("reply saved in '#{resultfile}'")
when /\%PDF\-/io then
report("done, file #{resultfile}, type pdf, #{total} chunks, #{File.size? rescue 0} bytes")
if resultfile =~ /\.pdf$/i then
report("file identified as 'pdf'")
elsif resultfile =~ /\..*$/o
report("result file suffix should be 'pdf'")
else
newresultfile = resultfile + '.pdf'
newresultfile.sub!(/\.pdf\.pdf/io, '.pdf')
pdf('close',newresultfile,autopdf)
begin
File.delete(newresultfile) if FileTest.file?(newresultfile)
resultfile = newresultfile if File.rename(resultfile,newresultfile)
rescue
report("adding 'pdf' suffix to result name failed")
else
report("'pdf' suffix added to result name")
end
end
report("result saved in '#{resultfile}'")
pdf('open',resultfile,autopdf)
status(replyfile,'ok') do
"<exa:filename>#{resultfile}</exa:filename>"
end
when /html/io then
report("done, file #{resultfile}, type html, #{total} chunks, #{File.size? rescue 0} bytes")
if resultfile =~ /\.(htm|html)$/i then
report("file identified as 'html'")
elsif resultfile =~ /\..*$/o
report("result file suffix should be 'htm'")
else
newresultfile = resultfile + '.htm'
begin
File.delete(newresultfile) if FileTest.file?(newresultfile)
resultfile = newresultfile if File.rename(resultfile,newresultfile)
rescue
report("adding 'htm' suffix to result name failed")
else
report("'htm' suffix added to result name")
end
end
report("result saved in '#{resultfile}'")
status(replyfile,'ok') do
"<exa:filename>#{resultfile}</exa:filename>"
end
else
report("no result file, first line #{firstline}")
status(replyfile,'no result file')
end
end
rescue TimeoutError
report("aborted due to time out")
status(replyfile,'time out')
rescue
report("aborted due to some problem #{traceback}")
status(replyfile,"no answer #{traceback}")
end
end
begin
report("run time: #{Time.now-start_time} seconds")
rescue
end
end
def start_dialogue(address, port, processtimeout)
timeout(@@connecttimeout) do
report("trying to connect to #{address}:#{port}")
begin
begin
if dialogue = Net::HTTP.new(address, port) then
# dialogue.set_debug_output $stderr
dialogue.read_timeout = processtimeout # set this before start
if dialogue.start then
report("connected to #{address}:#{port}, timeout: #{processtimeout}")
else
retry
end
else
retry
end
rescue
sleep(2)
retry
else
return dialogue
end
rescue TimeoutError
return nil
rescue
return nil
end
end
end
end
logger = Logger.new(banner.shift)
commandline = CommandLine.new
commandline.registerflag('autopdf')
commandline.registervalue('path' , '')
commandline.registervalue('request' , 'request.exa')
commandline.registervalue('reply' , 'reply.exa')
commandline.registervalue('result' , 'result')
commandline.registervalue('template' , '')
commandline.registervalue('file' , '')
commandline.registervalue('action' , '')
commandline.registervalue('timeout' , '')
commandline.registervalue('domain' , 'default')
commandline.registervalue('project' , 'default')
commandline.registervalue('username' , 'guest')
commandline.registervalue('password' , 'anonymous')
commandline.registervalue('exaurl' , 'exarequest')
commandline.registervalue('threshold' , '0')
commandline.registervalue('session' , '')
commandline.registervalue('address' , 'localhost')
commandline.registervalue('port' , '80')
commandline.registeraction('direct' , '[--path --request --reply --result --autopdf]')
commandline.registeraction('construct', '[--path --request --reply --result --autopdf] --file --action')
commandline.registeraction('extend' , '[--path --request --reply --result --autopdf] --file --action --template')
commandline.registeraction('direct')
commandline.registeraction('construct')
commandline.registeraction('extend')
commandline.registerflag('verbose')
commandline.registeraction('help')
commandline.registeraction('version')
commandline.expand
Commands.new(commandline,logger,banner).send(commandline.action || 'main')
|