Tuesday, February 10, 2009

XMLRPC over HTTP with Structured XML - VB.NET w/ Real-World Code

I've been working on a project for the past month on sending and receiving data to a vendor using XMLRPC over HTTP technologies. It proved to be a real bear to figure out as we got started as I couldn't see the format of XML I was sending and they never told me exactly what it needed to look like either. I've finally got it "licked" and thought I'd share some of the journey here in case it helps anyone else.

First off, I found it extremely helpful to use WireShark to analyze my network traffic. This finally gave me some specific information as to the XML I was sending and what I was getting back. I'd recommend it.

I found a set of C# classes that would do all the basics of XMLRPC over HTTP. I'm a VB.NET programmer by habit so this isn't my native language, but I haven't had to even touch this code, it works so well. You can get it at www.xml-rpc.net.

So, let's get into this. Using the basic functionality of XML over RPC, I originally came up with this bit of code that seemed to be sending data, but was constantly getting Internal Server Error (500) responses from the server.

Imports CookComputing.XmlRpc

Public Interface iGetClaim

<CookComputing.XmlRpc.XmlRpcMethod("api.server.clients.claims.search_for_client_claims")> _
Function SearchForClientClaims(ByVal api_id As String, ByVal api_password As String, ByVal client_id As String)

End Interface

Module Module1

Sub Main()

CallRPC()

End Sub


Sub CallRPC()
Dim Proxy As iGetClaim
Dim Protocol As XmlRpcClientProtocol
Dim XmlRpcHost As String = "http://api.xxx.com/rpc"
Dim ApiId As String = "aaa"
Dim Password As String = "bbb"
Dim ClientID As String = "ccc"

'Prepare the request
Proxy = CType(XmlRpcProxyGen.Create(Of iGetClaim)(), iGetClaim)
Protocol = CType(Proxy, XmlRpcClientProtocol)
Protocol.Url = XmlRpcHost

'Get the response
Dim result As String = Proxy.SearchForClientClaims(ApiId, Password, ClientID)
MsgBox(result)



End Sub

End Module




Once I got ahold of WireShark, I was able to analyze what was going out my network "door" and emailed the vendor the exact XML I was sending him:


<?xml version="1.0"?>
<methodCall>
<methodName>api.server.clients.claims.search_for_client_claims</methodName>
<params>
<param>
<value>
<string>aaa</string>
</value>
</param>
<param>
<value>
<string>bbb</string>
</value>
</param>
<param>
<value>
<string>ccc</string>
</value>
</param>
</params>
</methodCall>




This is when we started figuring it out. That's not what they were looking for. What they were looking for was something like this:
<?xml version="1.0"?>
<methodCall>
<methodName>api.server.clients.claims.search_for_client_claims</methodName>
<params>
<param>
<value>
<struct>
<member>
<name>api_id</name>
<value>
<string>aaa</string>
</value>
</member>
<member>
<name>api_password</name>
<value>
<string>bbb</string>
</value>
</member>
<member>
<name>client_id</name>
<value>
<string>ccc</string>
</value>
</member>
<member>
<name>paging</name>
<value>
<struct>
<member>
<name>offset</name>
<value>
<i4>0</i4>
</value>
</member>
<member>
<name>rows</name>
<value>
<i4>2</i4>
</value>
</member>
</struct>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>


Ok... How in the world do we fix that? I almost resorted to tossing the XMLRPC software and writing my own HTTP request and response handler and manually parsing the XML. I could have done that... but really, really, didn't want to. After much googling and a little hair pulling, we figured out how to structure the requests (and later the response) as a heirarchy of VB.NET Classes - XMLRPC code did the rest! Here's what a request winds up looking like in VB:

Imports CookComputing.XmlRpc

Public Interface iClaimInfo
<CookComputing.XmlRpc.XmlRpcMethod("api.server.clients.claims.get_claim")> _
Function GetClaim(ByVal value As Request) As Response

Class Request
Public api_id As String
Public api_password As String
Public claim_id As String
Public claim_number As String
End Class

Class Response
Public claim As Response_ClaimInfo
End Class

Class Response_ClaimInfo
Implements iClaimsFormsObject

Public Sub AddChildNodes(ByRef MyNode As System.Windows.Forms.TreeNode) Implements iClaimsFormsObject.AddChildNodes
'Do Nothing
End Sub

Public Sub ShowInPanel(ByRef Panel As System.Windows.Forms.Panel) Implements iClaimsFormsObject.ShowInPanel
Dim lbl As New Windows.Forms.Label
lbl.Text = XmlRpcInfo.GetProperties(Me)
lbl.Dock = Windows.Forms.DockStyle.Fill
Panel.Controls.Add(lbl)

End Sub

Public Sub DoubleClickNode(ByRef MyNode As System.Windows.Forms.TreeNode) Implements iClaimsFormsObject.DoubleClickNode
MsgBox(XmlRpcInfo.GetProperties(Me))
End Sub

Public client_id As String
Public client_parent_id As String
Public claim_status_description As String
Public claim_number As Integer
Public claim_status_key As String
Public claim_type_name As String
Public claim_created As String
Public adjuster As String
Public client_name As String
Public claim_location_of_loss_city As String
Public claim_date_of_loss As String
Public claim_client_claim_number As String
Public claim_id As String
Public adjuster_id As String
Public claim_location_of_loss As String
Public claim_received As String
Public last_modified As String
Public claim_priority As Integer
Public claim_completed As String
Public claim_type_id As String
Public client_parent_name As String
Public claim_last_worked As String
Public client_display_name As String


Public Function GetAmounts() As iClaimAmounts.Response
Dim a As New ClaimAmounts(claim_id)
Return a.GetAmounts()
End Function

Public Function GetPayments() As iClaimPayments.Response
Dim x As New ClaimPayments(claim_id)
Return x.GetData()
End Function

Public Function GetJournalEntries() As iClaimJournalEntries.Response_JournalEntry
Dim x As New ClaimJournalEntries(claim_id)
Return x.GetData()
End Function

End Class

End Interface

Public Class ClaimInfo

Private _ClaimId As String
Private _ClaimNumber As String

Public Sub New(ByVal ClaimId As String, ByVal ClaimNumber As String)
_ClaimId = ClaimId
_ClaimNumber = ClaimNumber
End Sub

Public Function GetClaim() As iClaimInfo.Response_ClaimInfo

Dim Proxy As iClaimInfo
Dim Protocol As XmlRpcClientProtocol
Dim Response As iClaimInfo.Response
Dim Request As iClaimInfo.Request

'Prepare the request
Proxy = CType(XmlRpcProxyGen.Create(Of iClaimInfo)(), iClaimInfo)
Protocol = CType(Proxy, XmlRpcClientProtocol)
Protocol.Url = XmlRpcInfo.XmlRpcHost

Try

'Send the Request
Request = New iClaimInfo.Request
Request.api_id = XmlRpcInfo.ApiId
Request.api_password = XmlRpcInfo.ApiPassword
Request.claim_id = _ClaimId
Request.claim_number = _ClaimNumber

'Receive the Response
Response = Proxy.GetClaim(Request)

'Return the claim
Return Response.claim

Catch ex As Exception

'Record any errors
MsgBox(ex.ToString)
Return Nothing

End Try

End Function

End Class

Here's an example of the returned XML that these classes can process:

<?xml version="1.0" encoding="us-ascii"?>
<methodResponse>
<params>
<param>
<value>
<struct>
<member>
<name>claim_list</name>
<value>
<array>
<data>
<value>
<struct>
<member>
<name>claim_status_key</name>
<value>
<string>C8</string>
</value>
</member>
<member>
<name>claim_id</name>
<value>
<string>aaa</string>
</value>
</member>
<member>
<name>employee_name</name>
<value>
<string>first last</string>
</value>
</member>
<member>
<name>employee_id</name>
<value>
<string>89DC932C-DE87-3BDC-9219-186A08A7C3E3</string>
</value>
</member>
<member>
<name>claim_number</name>
<value>
<int>xxx</int>
</value>
</member>
<member>
<name>claim_client_claim_number</name>
<value>
<string>xxx</string>
</value>
</member>
<member>
<name>claim_date_of_loss</name>
<value>
<string>03/31/2007</string>
</value>
</member>
<member>
<name>claim_insured</name>
<value>
<string>xxx xxx</string>
</value>
</member>
</struct>
</value>
<value>
<struct>
<member>
<name>claim_status_key</name>
<value>
<string>CD</string>
</value>
</member>
<member>
<name>claim_id</name>
<value>
<string>B3ADD56D-DD4D-3F9F-9D50-212489623927</string>
</value>
</member>
<member>
<name>employee_name</name>
<value>
<string>xxx xxx</string>
</value>
</member>
<member>
<name>employee_id</name>
<value>
<string>91ECC175-2074-3C24-B834-6A20CF807B58</string>
</value>
</member>
<member>
<name>claim_number</name>
<value>
<int>xxx</int>
</value>
</member>
<member>
<name>claim_client_claim_number</name>
<value>
<string>xxx</string>
</value>
</member>
<member>
<name>claim_date_of_loss</name>
<value>
<string>04/01/2007</string>
</value>
</member>
<member>
<name>claim_insured</name>
<value>
<string>xxx xxx</string>
</value>
</member>
</struct>
</value>
</data>
</array>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodResponse>


I've found that I had to kind of "guess" as to whether or not I would get back integers, strings, doubles.... the program would break if I got the wrong type, so I'm still tweaking that. I also developed a few "helper" functions to ease in the writing of code and identifying properties of the returned XML. I'll post a few here:

Public Class XmlRpcInfo
Public Shared XmlRpcHost As String
Public Shared ApiId As String
Public Shared ApiPassword As String
Public Shared ClientId As String

Shared Function GetKeys(ByVal coll As Object, Optional ByVal IncludeDims As Boolean = False) As String

Dim de As Object
Dim str As String = ""
Dim xml As XmlRpcStruct = coll(0)

For Each de In xml.Keys
If IncludeDims = True Then
str += " Public " & de & " as string" & vbCrLf
Else
str += de & vbCrLf
End If
Next

Return str
End Function



All in all, this is turning into one powerful XML over HTTP processor. It looks like I'm going to be able to use it not only for one client but for seveal other around the country, and I'm excited that I finally figured out the basics of how it works. If you have any questions or thoughts, please comment; I'd love to learn more.

0 comments:

Post a Comment

Blog Archive

Hits and Stats


Stats