Friday, October 19, 2007

ลองใช้ Ruby call Webservice ของสรรพากร

คุณ krunaporn เขียน แนะนำบริการ Web Service ของสรรพากร ใน blognone
ก็เลยจะลองใช้ ruby เรียกใช้ดูสักหน่อย
ดูสิว่าจะมีปัญหาหรืออุปสรรคอะไรบ้าง

เริ่มจาก Library ที่จะใช้ก่อน
ตรงนี้เราจะเลือกใช้ soap4r

จากนั้นก็เลือกบริการของสรรพากรที่เราจะทดสอบ
โดยเราจะเลือกบริการตรวจสอบเลขที่บัตรประชาชน ซึ่งมี wsdl อยู่ที่
https://rdws.rd.go.th/ServiceRD/CheckTINPINService.asmx?wsdl

ขั้นถัดไป ก็คือการ generate stub code จาก wsdl
โดยใช้ script ของ soap4r ที่ชื่อ wsdl2ruby.rb
ลองสั่ง

wsdl2ruby.rb --wsdl https://rdws.rd.go.th/ServiceRD/CheckTINPINService.asmx?wsdl --type client

จะได้ Error ตัวแรกออกมาก็คือ

at depth 0 - 20: unable to get local issuer certificate
F, [2007-10-19T14:41:50.023698 #12255] FATAL -- app:
Detected an exception. Stopping ... certificate verify failed (OpenSSL::SSL::SSLError)
/usr/lib/ruby/gems/1.8/gems/httpclient-2.1.2/lib/httpclient.rb:1039:in `connect'
/usr/lib/ruby/gems/1.8/gems/httpclient-2.1.2/lib/httpclient.rb:1039:in `ssl_connect'

ดูจาก error ก็จะเดาได้ว่า server certificate เป็นแบบ self sign ทำให้ OpenSSL มันฟ้อง error ออกมา
ตามไปแกะดูใน soap4r จะเห็นว่า soap4r มันใช้ library ที่ชื่อ httpclient
ซึ่งถ้าเราไปอ่านดูคู่มือ ก็จะเห็นว่ามันมี option ให้เลือกได้
ว่าจะ bypass การตรวจสอบ server certificate หรือไม่
แต่ปัญหาของเราก็คือ เราจะกำหนด option นั้นอย่างไร (เนื่องจาก script wsdl2ruby ไม่มีช่องทางให้ pass parameter ตัวนี้เลย)
การแก้ปัญหาที่ง่ายที่สุด ก็คือ
download เอา wsdl มาเก็บไว้ก่อน
จากนั้นก็ค่อยสั่ง
 
wsdl2ruby.rb --wsdl rdservice.wsdl --type client


ผลลัพท์จากการสั่ง wsdl2ruby.rb, เราจะได้ file ออกมา 4 file คือ
  • CheckTINPINServiceClient.rb
    file นี้เป็นโครงตัวอย่างการใช้งาน service
  • defaultDriver.rb
    file นี้เป็น Class ที่ represent service
  • defaultMappingRegistry.rb
    file นี้กำหนด mapping ของ parameter
  • default.rb
    file นี้เป็น class ที่ใช้ represent parameter กับ return value ของ service


ลองมาทดสอบเขียนโปรแกรมกัน
# เริ่มด้วยการ require soap4r
require 'rubygems'
gem 'soap4r'

# include generated library
require 'defaultDriver'

# initialize service
service = CheckTINPINServiceSoap.new()

# parameter
parameters = ServicePIN.new('anonymous', 'anonymous', '3100905022221')

# invoke service
resp = service.servicePIN(parameters)
puts resp

ลอง run ดู, จะได้ error ตัวเดิม
at depth 0 - 20: unable to get local issuer certificate
/usr/lib/ruby/gems/1.8/gems/httpclient-2.1.2/lib/httpclient.rb:1039:in `connect': certificate verify failed (OpenSSL::SSL::SSLError)
from /usr/lib/ruby/gems/1.8/gems/httpclient-2.1.2/lib/httpclient.rb:1039:in `ssl_connect'
from /usr/lib/ruby/gems/1.8/gems/httpclient-2.1.2/lib/httpclient.rb:1466:in `connect'

ทำไงดีหล่ะ จะ set option ไม่ให้ check server certificate อย่างไรดี
ค้นใน google ก็หาไม่เจอ
สุดท้ายต้องแกะเอาจาก source code ของ soap4r
ได้ความว่า ต้องใช้คำสั่งนี้
service.options['protocol.http.ssl_config.verify_mode'] = 
OpenSSL::SSL::VERIFY_NONE


อุปสรรคถัดไป ก็คือ มัน run ได้แล้วหล่ะ
แต่ผลลัพท์ที่ได้มันคืออะไร
ลองใช้ pretty printer สั่งพิมพ์ดู ได้ผลลัพท์นี้ออกมา

pp resp # =>
#<ServicePINResponse:0xb77b11e8
@servicePINResult=
#<ServicePINResponse::ServicePINResult:0xb77b10f8
@diffgram=
#<SOAP::Mapping::Object:0x..fdbbd53ca {}NewDataSet=#<SOAP::Mapping::Object:0x..fdbbd50b4
{}Message=#<SOAP::Mapping::Object:0x..fdbbd503c {}Code="E00008" {}Description="\340\271\200\340\270\245\340\270\202
PIN \340\271\204\340\270\241\340\271\210\340\270\226\340\270\271\340\270\201\340\270\225\340\271\211\340\270\255\340
\270\207 \340\271\200\340\270\231\340\270\267\340\271\210\340\270\255\340\270\207\340\270\210\340\270\262\340\270\201
\340\270\253\340\270\241\340\270\262\340\270\242\340\271\200\340\270\245\340\270\202PIN\340\271\200\340\270\233\340
\271\207\340\270\231\340\270\225\340\270\261\340\270\247\340\270\255\340\270\261\340\270\201\340\270\251\340\270\243
\340\270\253\340\270\243\340\270\267\340\270\255\340\271\200\340\270\245\340\270\202PIN\340\271\200\340\270\233\340
\271\207\340\270\231\340\270\225\340\270\261\340\270\247\340\270\255\340\270\261\340\270\201\340\270\251\340\270\243
\340\270\253\340\270\243\340\270\267\340\270\255\340\270\210\340\270\263\340\270\231\340\270\247\340\270\231\340\270
\253\340\270\245\340\270\261\340\270\201\340\271\200\340\270\227\340\271\210\340\270\262\340\270\201\340\270\261\340
\270\23213\340\270\253\340\270\245\340\270\261\340\270\201\340\270\253\340\270\243\340\270\267\340\270\255\340\270\243
\340\270\271\340\270\233\340\271\201\340\270\232\340\270\232 PIN\340\271\204\340\270\241\340\271\210\340\270\226\340
\270\271\340\270\201\340\270\225\340\271\211\340\270\255\340\270\207 <br> PIN incorrect <br> => \"3100905017911x\"">>>,
@schema=
#<SOAP::Mapping::Object:0x..fdbbd87f0 {http://www.w3.org/2001/XMLSchema}element=#<SOAP::Mapping::Object:0x..fdbbd8782
{http://www.w3.org/2001/XMLSchema}complexType=#<SOAP::Mapping::Object:0x..fdbbd870a {http://www.w3.org/2001/XMLSchema}
choice=#<SOAP::Mapping::Object:0x..fdbbd8692 {http://www.w3.org/2001/XMLSchema}element=#<SOAP::Mapping::Object:0x..fdbbd861a
{http://www.w3.org/2001/XMLSchema}complexType=#<SOAP::Mapping::Object:0x..fdbbd85a2 {http://www.w3.org/2001/XMLSchema}
sequence=#<SOAP::Mapping::Object:0x..fdbbd852a {http://www.w3.org/2001/XMLSchema}element=["", ""]>>>>>>>>>

xml หนอ xml
แกะจากตรงนี้ มันปวดตา ไปหน่อย มี ruby class ปนขึ้นมาด้วย
เปลี่ยนไป แกะจาก message จริงๆที่ส่งกลับมาดีกว่า

เพื่อจะดู message จริง ก็ให้ใส่ debug flag ตัวนี้ลงไป

service.wiredump_dev = STDERR if $DEBUG

จากนั้นเวลา run ก็ให้ใส่ flag -d ให้ ruby ด้วย (ruby -d)
ซึ่งมันจะแสดง message จริงออกมาดังนี้
<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body><ServicePINResponse xmlns="https://rdws.rd.go.th/ServiceRD/CheckTINPINService">
<ServicePINResult><xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"><xs:element name="NewDataSet"
msdata:IsDataSet="true"><xs:complexType><xs:choice maxOccurs="unbounded">
<xs:element name="Message"><xs:complexType><xs:sequence><xs:element name="Code"
type="xs:string" minOccurs="0" /><xs:element name="Description" type="xs:string" minOccurs="0" />
</xs:sequence></xs:complexType></xs:element></xs:choice></xs:complexType></xs:element></xs:schema>
<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
<NewDataSet xmlns=""><Message diffgr:id="Message1" msdata:rowOrder="0" diffgr:hasChanges="inserted">
<Code>E00008</Code><Description>เลข PIN ไม่ถูกต้อง เนื่องจาก
หมายเลขPINเป็นตัวอักษรหรือเลขPINเป็นตัวอักษรหรือจำนวนหลักเท่ากับ13หลักหรือรูปแบบ PINไม่ถูกต้อง
&lt;br&gt; PIN incorrect &lt;br&gt; =&gt; "3100905017911x"</Description></Message></NewDataSet>
</diffgr:diffgram></ServicePINResult></ServicePINResponse></soap:Body></soap:Envelope>

ดูแล้วปวดตามาก แต่วิธีไล่ที่ง่ายที่สุด ก็คือ
ดูจากหลังไปหน้า เริ่มที่บรรทัดท้ายสุดเลย
จะเห็นว่า response มันมีโครงประมาณนี้

+ ServicePINResponse
+ ServicePINResult
+ diffgram
+ NewDataSet
+ Message
+ Code
+ Description

ลองสั่งพิมพ์ผลลัพท์ด้วยคำสั่งนี้

puts resp.servicePINResult.diffgram.newDataSet.Message.Description

ผลลัพท์คือ No Method Found.
หลังจากแงะดู response อยู่อีกพักใหญ่ (เข้าใจว่านานพอควร)
ก็ได้ข้อสรุปว่า
ต้องเรียกอย่างนี้ ถึงจะถูก

puts resp.servicePINResult.diffgram.newDataSet['Message']['Code']
puts resp.servicePINResult.diffgram.newDataSet['Message']['Description']

ทำไมข้อมูลใน newDataSet, soap4r มันแปลงเป็น hash ให้หว่า?

ข้อคิดที่ได้
+ ความตั้งใจดี แต่ design response คิดสั้นไปนิด

Related link from Roti

Thursday, October 18, 2007

ไป Codefest กันดีกว่า

หลังจากเข้าร่วมงาน Codefest ครั้งแรกเมื่อปีก่อนมาแล้ว
โดยปีก่อนนู้นทดลองเอา Erlang มา implement Vehicle tracking Server
ตอนนี้ก็ได้ฤกษ์หาโครงการเข้าร่วมอีกครั้ง
แต่ด้วยความโลภ ส่งโครงการเดียวไม่สนุก
ปีนี้เลยเอาเสียสองโครงการ

  • โครงการแรกไม่ได้ฉีกไปจากแนวเก่านัก
    โดยจะทำเป็น server ที่ aggregate ข้อมูลจาก Embedded Device ผ่านทาง GPRS
    แล้วก็ present ข้อมูลร่วมกับ MapServer
    แต่จะเน้นไปที่อุปกรณ์จริงมากขึ้น
    โดยปีนี้ได้ พี่ปู่ จาก Embeddedj มาช่วย Implement Embedded Device ให้ทดลองเล่นกัน

    ส่วนตัว server เนื่องจากไม่มีประเด็นเรื่องการรับ load แบบคราวก่อน
    ปีนี้จึงเปลี่ยนมาใช้ Rails แทน

    ส่วนการ Present ผ่าน Map
    เพื่อลดการพึ่งพา Google Map ก็เลยจะลองใช้
    MapServer แทน

  • โครงการที่สอง เป็นการเชื่อมระหว่าง Adobe Air กับ Mozart Oz
    ทำเป็นโปรแกรมจัดตารางห้องประชุม (ที่ใช้ constraint programming จากฝั่ง Oz ช่วย solve)


ใครว่างเชิญไปร่วมสนุกกันได้นะครับ Codefest 2007

Update: เพื่อความท้าทาย, โครงการที่สอง เปลี่ยนใจไปใช้ Air + Erlang ทำ โปรแกรมประมูล แทน

Related link from Roti

Monday, October 15, 2007

javascript interval

น้อง sand ถามปัญหามาเรื่อง จะใช้ dojo ดึงข้อมูลจาก server ทุกๆ 30 วินาที

ใน javascript มันมี function ที่ชื่อ interval ให้ใช้
window.onLoad=function() {
window.setInterval(myrefresh, 30000);
}

function myrefresh() {
... do something
}

ถ้าเราอยากดึงข้อมูลจาก server ก็แค่ใส่ logic ให้ไปใช้ function dojo.xhrGet
เพื่อดึงข้อมูลแบบ ajax มา update content บนหน้าจอ

ข้างบนเป็นแบบง่ายสุด
ถ้าต้องการ feature มากขึ้นเช่น
start, stop interval ได้
dojo ก็มี object ที่ชื่อ dojox.timing.Timer ให้ใช้
วิธีใช้ ก็ตามนี้
dojo.require("dojox.timing._base");

dojo.addOnLoad(function() {
var timer = new dojox.timing.Timer(30 * 1000);
timer.onTick=function() {
... do something
}
timer.start();
});


หรือถ้าเรามีเหตุการณ์ที่ต้องการให้เกิดตาม sequence ที่กำหนด
ก็ใช้ object dojox.timing.Sequence ก็ได้
ลองดูตัวอย่าง วิธีการกำหนด sequence
var seq = [
{func: [showMessage, window,
"i am first"], pauseAfter: 1000},
{func: [showMessage, window,
"after 1000ms pause this should be seen"], pauseAfter: 2000},
{func: [showMessage, window,
"another 2000ms pause and 1000ms pause before"], pauseAfter: 1000},
{func: [showMessage, window,
"repeat 10 times and pause 100ms after"], repeat: 10, pauseAfter: 100},
{func: returnWhenDone} // no array, just a function to call
];

Related link from Roti