Jak wywołać dowolny web service bez WSDL i bez generowania kodu
W dzisiejszym artykule zajmiemy się wywoływaniem web services z poziomu procesu NGinn, przy okazji poznając alternatywny sposób korzystania z web services. Jak pewnie doskonale wszyscy wiedzą, web services są komponentami (uslugami) dostępnymi zdalnie, za pośrednictwem niezależnego od platformy protokołu SOAP. Wywołanie web service jest niczym innym jak przekazaniem komunikatu SOAP oraz odebraniem odpowiedzi (też SOAP), najczęściej za pomocą protokołu http.
Jednak gdy programista chce w swojej aplikacji wywołać jakiś web service zwykle nie posługuje się on bezpośrednio SOAPem, ale używa dostępnych w danym języku narzędzi ‘opakowujących’ które pozwalają mu wywoływać web service tak jakby to było wywołanie lokalnej funkcji . W Microsoft.Net posługujemy się na przykład narzędziem WSDL.exe które na podstawie XML-owego opisu usługi (WSDL) generuje nam kod w C# (lub VB) który opakowuje nam web service do postaci ‘strawnej’ dla .Net, czyli obiektu określonej klasy z interfejsem zgodnym z definicją web service. Jest to na tyle wygodne i uniwersalne że w typowych zastosowaniach możemy polegać tylko na wsdl.exe i w ogóle zapomnieć o XML-u i http pod spodem. Ale czasami wsdl.exe nie wystarcza – na przykład gdy nie wiemy z góry jaki web service będziemy wywoływać.
WSDL.exe generuje kod opakowujący web service, co oznacza że struktura web service jest wkompilowana w program który z danej usługi korzysta – i gdy usługa się zmieni to należy ponownie wygenerować kod wsdl.exe’m i skompilować aplikację od nowa. Świetnie, ale gdy aplikacja musi się komunikować z web service którego nie znamy na etapie kompilacji to WSDL.exe do niczego się nie przyda. NGinn jest właśnie takim przypadkiem ponieważ oferuje możliwość wywoływania web services w ramach procesów. Wszelkie potrzebne do wywołania informacje czerpie z definicji procesu a na podstawie tych informacji tworzy i wysyła komunikaty SOAP.
Żeby wywołać web service z procesu NGinn trzeba posłużyć się zadaniem ‘XmlHttp’. Jest to zadanie pozwalające na wykonywanie wywołań http, między innymi takich które zawierają XML. Żeby wywołać web service trzeba tylko zadbać o to aby ten XML był SOAP-em o strukturze której spodziewa się nasz web service – czyli zapewnić ‘binding’ między danymi na których operuje nasz proces a odpowiednimi polami w komunikacie SOAP. Na aktualnym etapie rozwoju NGinn robi się to za pomocą transformacji XSLT. Żeby było łatwiej to wszystko przyswoić posłużmy się przykładem. Jest taka publicznie dostępna usługa http://aspspider.info/trutech/MarketWatch.asmx która pozwala m.in . zapytać się o kursy akcji, indeksy giełdowe i kursy metali szlachetnych. Udostępnia ona operację ‘GetQuote’ za pomocą której będziemy odpytywać się o kursy akcji. Po wejściu na w/w stronę ukazuje się nam opis usługi (standardowo wygenerowany przez ASP.Net) z WSDL oraz z przykładami komunikatów SOAP. No i przykładowy komunikat SOAP dla zapytania o kurs akcji Microsoftu wygląda tak:
[xml]
<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<GetQuote xmlns="http://trutech.com/marketwatch/">
<credValidate>
<Email>trutech.shared@gmail.com</Email>
<Password>trutech.shared@gmail.com</Password>
</credValidate>
<MyScript>
<sScriptCode>MSFT</sScriptCode>
</MyScript>
</GetQuote>
</soap12:Body>
</soap12:Envelope>
[/xml]
Gdy wyślemy taki XML POST-em na adres http://aspspider.info/trutech/MarketWatch.asmx dostaniemy odpowiedź która wygląda mniej-więcej tak:
[xml]
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<GetQuoteResponse xmlns="http://trutech.com/marketwatch/">
<GetQuoteResult>true</GetQuoteResult>
<MyScript>
<sScriptCode>MSFT</sScriptCode>
<sScriptName>MSFT – Microsoft Corpora</sScriptName>
<sScriptNameFromServer>Microsoft Corpora</sScriptNameFromServer>
<sScriptStockExchange>NasdaqNM</sScriptStockExchange>
<sScriptLastPrice>28.22 Rs</sScriptLastPrice>
<sScriptPreviousClose>28.22 Rs</sScriptPreviousClose>
<sScriptChangeInRupees>0.00 Rs</sScriptChangeInRupees>
<sScriptChangeInPercentage>0.00 %</sScriptChangeInPercentage>
<sScriptVolume>0 Sh</sScriptVolume>
<sScriptDaysRange>N/A – N/A</sScriptDaysRange>
<sScriptYearRange>14.87 Rs – 29.35 Rs</sScriptYearRange>
<sScriptToolTipText>
Registration Code : MSFT
Company Name : MSFT – Microsoft Corpora
Exchange : NasdaqNM ( 2009-10-29 )
Last Price : 28.22 Rs
Previous Close : 28.22 Rs
Change (INR) : 0.00 Rs
Change (%) : 0.00 %
Trading Volume : 0 Shares
Price Range (Day) : N/A – N/A
Price Range (52 Weeks) : 14.87 Rs – 29.35 Rs
Query Time : 2009-10-30 17:02:57
</sScriptToolTipText>
<sScriptWebLink>http://in.finance.yahoo.com/q?s=MSFT</sScriptWebLink>
<sScriptLastTradeDate>2009-10-29</sScriptLastTradeDate>
<sScriptLastTradeTime>4:00pm</sScriptLastTradeTime>
<sScriptQueryDateTime>2009-10-30 17:02:57</sScriptQueryDateTime>
<bScriptDataFound>true</bScriptDataFound>
<lScriptRowId>0</lScriptRowId>
</MyScript>
</GetQuoteResponse>
</soap:Body>
</soap:Envelope>
[/xml]
Interesująca nas cena jest w polu ‘sScriptLastPrice’. Niektórzy pewnie się zastanawiają co oznacza ‘Rs’ obok ceny – otóż jest to indyjski web service podający ceny w Rupiach. Bardzo przepraszam ale innego nie znalazłem.
Skoro już wiemy jak wyciągnąć cenę akcji Microsoftu w Rupiach to zbudujmy proces NGinn który dokładnie to robi – tj na wejściu bierze symbol spółki giełdowej i odpytuje się o cenę akcji tej spółki. Proces ten będzie miał jedną zmienną wejściową – symbol spółki, i jedną zmienną wyjściową – kurs akcji wyrażony w rupiach.
[xml]
<?xml version="1.0" encoding="utf-8"?>
<process version="1" name="RupeeStockQuote" xmlns="http://www.nginn.org/WorkflowDefinition.1_0.xsd">
<variables>
<variable name="symbol" type="string" required="true" dir="In" />
<variable name="price" type="string" required="true" dir="Out" />
</variables>
<body>
<places>
<place id="start" type="Start" />
<place id="end" type="End" />
</places>
<tasks>
<task id="T1" type="XmlHttp">
<variables>
<variable name="symbol" type="string" dir="In" required="true"/>
<variable name="price" type="string" required="true" dir="Out" />
</variables>
<parameters>
<param variable="Url"><value>http://aspspider.info/trutech/MarketWatch.asmx</value></param>
<param variable="RequestType"><value>SOAP</value></param>
<param variable="ResponseType"><value>SOAP</value></param>
<param variable="HttpMethod"><value>POST</value></param>
<param variable="RequestBodyXsl"><value>GetQuote_Request.xsl</value></param>
<param variable="ResponseBodyXsl"><value>GetQuote_Response.xsl</value></param>
<param variable="RequestHeaders"><expr>{"Content-Type": "application/soap+xml; charset=utf-8"}</expr></param>
</parameters>
</task>
</tasks>
<flows>
<flow from="start" to="T1" />
<flow from="T1" to="end" />
</flows>
</body>
</process>
[/xml]
Proces ten jest bardzo uproszczony – ma tylko jedno zadanie. W normalnej sytuacji miałby on dalszy ciąg, czyli logikę która w jakiś sposób działa w oparciu o cenę akcji i np. wysyła do kogoś powiadomienie. Ale to tylko przykład więc musi być prosty.
Widać tu że zadanie ‘T1’ posługuje się dwiema transformacjami XSL – pierwsza z nich jest zapisana w pliku ‘GetQuote_Request.xsl’ i służy do wygenerowania komunikatu SOAP wywołującego web service, druga, w pliku ‘GetQuote_Response.xsl’ służy do przetworzenia odpowiedzi web service na postać zrozumiałą dla NGinn. Oto te transformacje:
GetQuote_Request.xsl
[xml]
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="utf-8" indent="yes" />
<xsl:template match="/data">
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<GetQuote xmlns="http://trutech.com/marketwatch/">
<credValidate>
<Email>trutech.shared@gmail.com</Email>
<Password>trutech.shared@gmail.com</Password>
</credValidate>
<MyScript>
<sScriptCode><xsl:value-of select="symbol" /></sScriptCode>
</MyScript>
</GetQuote>
</soap12:Body>
</soap12:Envelope>
</xsl:template>
</xsl:stylesheet>
[/xml]
Na wejściu dostaje ona XML zawierający dane zadania ‘T1’, czyli coś w rodzaju
[xml]<data><symbol>MSFT</symbol></data>[/xml]
Na wyjściu natomiast powstaje komunikat SOAP o strukturze opisanej wcześniej – nasza transformacja po prostu wstawia zmienną ‘symbol’ w polu ‘sScriptCode’. Po wygenerowaniu komunikatu SOAP zadanie T1 wysyła go w treści requestu ‘POST’ pod adres web service. Następnie odbiera odpowiedź, czyli opisany wyżej komunikat ‘GetQuoteResponse’, który z kolei poddaje transformacji ‘GetQuote_Response.xsl’:
[xml]
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:quot="http://trutech.com/marketwatch/">
<xsl:output method="xml" encoding="utf-8" indent="yes" />
<xsl:template match="/soap:Envelope/soap:Body/quot:GetQuoteResponse">
<data>
<price><xsl:value-of select="quot:MyScript/quot:sScriptLastPrice" /></price>
</data>
</xsl:template>
</xsl:stylesheet>
[/xml]
Widać że na wyjściu otrzymamy bardzo prosty XML w rodzaju
[xml]<data><price>28.08 Rs</price></data>[/xml]
NGinn na podstawie tego XML zaktualizuje zmienne zadania T1, czyli przypisze otrzymaną od web service wartość kursu akcji do zmiennej ‘price’. Na tym zadanie ‘T1’ się kończy, a razem z nim cały przykładowy proces.
Jest to bardzo elastyczna metoda wywoływania web services, mamy praktycznie całkowitą kontrolę nad strukturą przekazywanych komunikatów. Niestety, dzieje się to kosztem dość żmudnego przygotowywania XSL-i – nie ma żadnego narzędzia które by to zrobiło za nas (pewnie takie narzędzie korzystało by z WSDL, my nie musimy bo mamy przykłady XML-i). Warto tu wspomnieć o narzędziach które przydają się do tworzenia w/w xsl-i i w ogóle do testów web services. Pierwszym z tych narzędzi jest pakiet ‘Curl’ (http://curl.haxx.se/ ) umożliwiający wysyłanie po http dowolnych żądań (a w szczególności naszych komunikatów SOAP). Drugim jest pakiet ‘nxslt’ (http://www.xmllab.net/downloads/nxslt/) umożliwiający testowanie działania transformacji XSLT. Posługując się tymi dwoma narzędziami jesteśmy w stanie szybko ustalić jaka powinna być struktura komunikatów SOAP i wygenerować odpowiednie arkusze XSL. W ten sam sposób możemy obsłużyć nie tylko wywołania web services, ale np protokół XML-RPC czy inne, oparte na XML/HTTP protokoły komunikacji co czyni zadanie XmlHttp dość uniwersalnym narzędziem.
Taki sposób używania web services nie jest oczywiście zarezerwowany dla NGinn – można to robić w każdej innej sytuacji, nawet gdy mamy do dyspozycji Visual Studio z automatycznym generowaniem kodu klienta web service. Po co? Otóż są pewne typy danych których standardowa serializacja SOAP oferowana przez Microsoft.Net nie potrafi obsłużyć – na przykład typy nullable, nieprawidłowo też są obsługiwane pola typu ‘DateTime’ w innej niż UTC strefie czasowej. Innym przykładem są niestandardowe nagłówki http lub rozszerzenia SOAP – za pomocą opisanego tu sposobu możemy generować wywołania z dowolnymi nagłówkami i z dowolną treścią, podczas gdy w ‘normalny’ sposób musieli byśmy pisać w C#/VB dodatkowy kod obsługujący przypadki niestandardowe. No a poza tym jest to świetny sposób testowania web services bez pisania kodu.
Ponieważ opisany tu mechanizm wywoływania web services jest bardzo prosty, można wręcz powiedzieć że prymitywny, rodzi się pytanie czy jest to rozwiązanie docelowe czy tylko prowizoryczne? Na pewno brakuje mu kilku rzeczy – między innymi automatycznej kontroli zgodności schematów danych z definicją web service, można by się też pokusić o jakiś graficzny edytor pozwalający mapować zmienne zadania na parametry we/wy usługi lub też automatycznie generować XSL-e na podstawie jakiegoś prostszego mapowania. Ale mimo to uważam że jest to elastyczne i proste w użyciu narzędzie które da się zastosować w wielu sytuacjach.