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

1 comment:

OrangeGears Project said...

ใน ofbiz มี xmlrcp ด้วยครับแต่ไม่เคยลอง

http://localhost:8080/webtools/control/xmlrpc