Hanson Brothers Interoperability Sample: Overview
Clarity Consulting, Inc. Summary: Demonstrates the SOAP Toolkit for Visual Studio 6.0 and how it can be used to interact with a Web Service. This sample displays interaction between two Web Services: one that resides on a Windows 2000 server and a second that resides on a Sun Solaris server.
ContentsHanson
Brothers Business Scenario Hanson Brothers Business ScenarioHanson Brothers is an online retailer who recently expanded their product offerings through the acquisition of a sporting goods company. They wanted to integrate their e-commerce site with an operational system from the recently acquired sporting goods company. The sporting goods company's inventory management system runs on a Sun Solaris server. The company's product and inventory information is maintained in an Oracle database and the data is accessed through Common Object Request Broker Architecture (CORBA) components. Hanson Brothers' e-commerce and order management systems are based on the Windows DNA application architecture and run on Microsoft® Windows 2000® servers. Rather than rewrite the sporting goods inventory management system, the Hanson Brothers IT department decided to integrate the systems using the Simple Object Access Protocol (SOAP) Toolkit for Visual Studio® 6. In addition to reusing existing systems, Hanson Brothers wanted to provide systems to support its business partners. Hanson Brothers created an additional Web Service to allow partners to browse their catalog and add orders. HBInterop GoalsIn writing the HBInterop sample, our first goal was to provide a demonstration of how COM+ and components could be integrated using SOAP. Next, we wanted to demonstrate how SOAP could be used to integrate components on distinct platforms such as Windows 2000 and UNIX. Finally, we thought this sample provided a great opportunity to demonstrate the use of the SOAP Toolkit for Visual Studio 6.0 and how it simplifies the process of calling and implementing a Web Service. To meet these goals we:
The resulting sample demonstrates not only the reuse of COM+ components by remote users, but how COM+ and CORBA components residing on distinct platforms can be integrated using SOAP. HBInterop ArchitectureThe core elements of the HBInterop sample include:
Figure 1. HB retail site architecture. Note that the business logic resides in the HBInterop dll. The Product data class makes calls to the Web Service wrapper represented in Figure 2 below.
Figure 2. Product Web Service interoperability. HBProduct resides on a Windows 2000 server and wraps calls to the Product Web Service on the Sun Solaris server. As HBProduct processes a request for product information, the proxy object is invoked and a SOAP request is generated. Next, the Product Web Service on Sun Solaris receives a SOAP request, which is parsed by the Listener and the related CORBA object is called.
Figure 3. HB Interop Web Service. This Web Service, which provides business partners with the ability to browse products and save orders, exposes methods of the HBProduct and HBInterop components.
Calling a Web ServiceThe BrowseByCategory method of HBProduct uses the Remote Object Proxy Engine (ROPE) from the SOAP Toolkit for Visual Studio 6.0 to generate SOAP requests. ROPE includes the infrastructure needed to build a client that consumes Web Services, as well as to build a "SOAP listener" that can receive, parse, and dispatch SOAP messages sent to a URL. Before the ROPE proxy object can call the Product Web Service, the Service Description Language (SDL) needs to be loaded so the proxy knows how to structure calls. SDL is an Extensible Markup Language (XML) format a Web server can use to describe the messages it supports, and the protocols that are supported for receiving those messages. The SDL file performs a function for a Web Service that is similar to the function a type library performs for a COM+ component. This SDL file contains the information necessary to properly format calls to a Web Service and parse the responses. To download or get information on the SOAP Toolkit for Visual Studio
6.0, please visit the MSDN XML Developer Center Here is an example of how SDL is obtained from the Product Web Service: Set objProxy = New ROPE.Proxy objProxy.LoadServicesDescription(icURI, "http://192.168.1.1/Inventory.xml") Once the SDL is loaded, the proxy object can be used to call a method on the Product Web Service and generate a SOAP request. Here is an example of how the BrowseCategoryID method of the Web Service is called: intCategoryID = 1 strReturn = objProxy.BrowseByCategoryID(intCategoryID) In this case, the Product Web Service receives a request indicating that the BrowseByCategoryID method needs to be executed. The Web Service executes the method and then replies with a SOAP message containing the response parameter. The response parameter contains products that match the category ID and the product information is marked up as XML. Listening for SOAP RequestsThe Web Service listener that resides on the Solaris server is written in Java. The SOAP request is initially processed by the start function of the HTTPThread class. The request is then passed to the processPostRequest function where the following lines invoke the SOAP parser: ProductSoapConverter sc = new _ ProductSoapConverter(request.getBody().getBytes()); The SOAPConverter uses an XML SAX parser, which provides an event driven API for decomposing documents. Here is how a SOAP request for the BrowseByCategoryID method would be processed. As the request is parsed the startElement event of the SoapHandler class fires each time a new element is encountered. In this event, we have logic to check for certain elements (or tags). Once we hit the bodySection tag, we begin checking for elements that contain a reference to a supported method. When we encounter such a method tag, we create an instance of the related class. In this case, the inventoryBrowseByCategoryIDMethod is encountered, and an instance of the InventoryBrowseByCategoryID class is created. As we encounter elements with parameter values, we will set corresponding properties so all the information we need to execute the request is maintained by InventoryBrowseByCategoryID when we are done parsing the request. Below is a subset of the code in the startElement event. Note that some of the names have been abbreviated and are different from what you will see the Listen.java file. if(name.equals(inventoryCommit.Method))
{
currentMethod = name;
ic = new InventoryCommit();
}
else if (name.equals(categoryBrowseAll.Method))
{
currentMethod = name;
cba = new CategoryBrowseAll();
}
else if (name.equals(inventoryBrowseByCategoryID.Method) )
{
currentMethod = name;
ibc = new InventoryBrowseByCategoryID();
}
Because we haven't hit the end tag yet, the startElement event continues to fire and we now begin checking for arguments. When we hit an argument tag, we set the currentArgument variable to the name of the current argument. We will use this information in a parsing event that fires immediately after the startElement event. Here is a subset of the code related to parsing the names of arguments. else if (currentMethod.equals(inventoryCommit.Method))
{
if(name.equals(inventoryCommit.skuVar)
{
currentArgument = name;
}
else if(name.equals(inventoryCommit.desQtyVar)
{
currentArgument = name;
}
}
else if (currentMethod.equals(inventoryBrowseByCategoryID.Method))
{
if (name.equals(inventoryBrowseByCategoryID.catIdVar)
{
currentArgument = name;
}
}
Next the characters event fires whenever an element has characters between its begin and end tags. As we continue to parse our SOAP request, we will use this event to capture the value of the id argument and assign it to the ID property on the InventoryBrowseByCategoryID object. Here is a subset of the code related to parsing an arguments value. else if (currentMethod.equals(inventoryBrowseByCategoryID.Method))
{
if (currentArgument.equals(idArgument))
{
ibc.SetID(Integer.parseInt(value));
}
}
The endElement event fires when the closing element is encountered. We used this event to kick-off the execute method on the InventoryBrowseByCategoryID object. In the execute method, we make our call to the CORBA object and prepare the SOAP response. Here is the line where the CORBA object is called: Product.InventoryPackage.Item[] invList =
getBinding().browseByCategoryId(getId());
An array is returned from the call and the execute method understands how to transform the array contents into XML. The XML is wrapped in CDATA tags and is incorporated into the SOAP response. The response is stored in the Response property of the InventoryBrowseByCategoryID object and is available in the processPostRequest function where the following lines send the response back to the client. sendText(outStream, request.wrapResponse(sh.getInventoryBroseByCategoryID().getResponse() )): Preserving the MarkupThe return type of BrowseByCategoryID in the Product Web Service is defined as a string. The string is marked up as XML to preserve the complexity of the data, which contains columns and rows from a database query. Though this makes sense, there is an issue because SOAP itself is XML and the SOAP parser doesn't know where to stop parsing. To solve this problem, we use the CDATA data tag, which tells the parser not to process the data between these tags. Here is an example of the contents of the results string prior to parsing: <![CDATA[ <Inventory> <Item> <SKU>501</SKU> <Description>Mid Weight Hiking Boot</Description> <Price>80</Price> <AvailableQuantity>50</AvailableQuantity> </Item> </Iventory> ]]> The SOAP parser only strips off the CDATA tags so the remainder of the XML is preserved. The relative meaning of the information is preserved and it can be made available to other COM+ users. Inside the HBProduct dll, we set the return of the BrowseProductByCategory function equal to the string that was returned from the Product Web Service. Since we intend to expose the BrowseProductByCategory method on our HBInterop Web Service, we need to re-add the CDATA tags so the XML is preserved. In the call below, we wrap the results of our Product Web Service call with CDATA: BrowseByCategory = "<![CDATA[" & objProxy.BrowseByCategoryID(CategoryID) & "]]>" Converting XML Data to an ADO RecordsetThe HBInterop component returns ActiveX® Data Objects (ADO) recordsets to its consumers and the product data class, ProductDC, obtains product information from HBProduct as XML. In order to maintain consistency in the HBInterop interface, we need to get the XML data supplied by HBProduct into an ADO recordset. We accomplish this by using Extensible Stylesheet Language (XSL) to transform the XML into an attribute rich XML document that can be used to open an ADO recordset. The string returned by HBProduct begins with CDATA, which is not well-formed XML and the Document Object Model (DOM) will not process it. Before we can load the XML into the DOM, we use the simple string functions below to remove the CDATA tags. strResponse = objProduct.BrowseByCategory(CategoryID) strResponse = Right(strResponse, (Len(strResponse) - 9)) strResponse = Left(strResponse, (Len(strResponse) - 3)) Once CDATA has been removed, we load the resulting string into an instance of the DOM so we can prepare to transform it into an attribute rich XML document. domSource.async = False Call domSource.loadXML(strResponse) Then we load XSL into another instance of the DOM by calling the GetInventorytoRSxsl function and supplying its results to the loadXML method. domStyle.async = False Call domStyle.loadXML(GetInventoryToRSxsl) The XSL returned by GetInventoryToRSxsl contains the rules for transforming the element rich markup that was returned by HBProduct into the attribute rich markup that ADO expects. For each Item element in the original XML document, a z:row tag is added. Then the value of the SKU, Description, Price, and Inventory elements are added to z:row attributes of the same name. <rs:data> <xsl:for-each select="Item"> <z:row> <xsl:attribute name="SKU"> <xsl:value-of select="SKU" /> </xsl:attribute> <xsl:attribute name="Name"> <xsl:value-of select="Description" /> </xsl:attribute> <xsl:attribute name="Price"> <xsl:value-of select="Price" /> </xsl:attribute> <xsl:attribute name="Inventory"> <xsl:value-of select="AvailableQuantity" /> </xsl:attribute> </z:row> </xsl:for-each> </rs:data> In addition to transforming the XML, the XSL template adds the schema information that ADO expects. The full XSL used for the transformation is available in Appendix A of this document. The transformation occurs when domStyle, the instance of the DOM containing the XSL, is passed to the transformNode method on domSource, which contains the response string. The ADO compliant XML that is returned is stored in the strResults variable. strResults = domSource.transformNode(domStyle) Prior to the transformation, an individual product was marked up like this: <Item> <SKU>501</SKU> <Description>Mid Weight Hiking Boot</Description> <Price>80</Price> <AvailableQuantity>50</AvailableQuantity> </Item> After the transformation, a product is marked up like this: <z:row SKU="501" Name=" Mid Weight Hiking Boot" Price="80" Inventory="50" /> Now that we have XML that ADO understands, we can open a recordset. We do this by reloading the ADO compliant XML into an instance of the DOM and passing the DOM to the Open method of the recordset: Call domSource.loadXML(strResults) Call rs.Open(domSource) Changing the Configuration Information at RuntimeIn the past, developers have used the registry to store configuration information, which often changes after an application has been compiled. Sometimes problems arise because special permissions might have to be set on the registry so the information can be read. To avoid these issues, we use the constructor string that was introduced with COM+. To do this we implement the IObjectConstruct interface in our classes and then a string can be passed to components when they are created by Component Services. The constructor string we are using contains name value pairs, allowing us to pass multiple parameters. Inside the IObjectConstruct_Construct function, we parse the string and store the values in module level variables. Here is an example of how we parse a constructor string: Private Sub IObjectConstruct_Construct(ByVal objCtor As Object)
arrParams = Split(strConstructor, ",")
or intCount = LBound(arrParams) To UBound(arrParams)
Select Case UCase(GetKey(arrParams(intCount)))
Case UCase("ConnectionString")
m_strConnection = GetValue(arrParams(intCount))
Case UCase("CORBA_ServiceURI")
m_strCORBA_ServiceURI = GetValue(arrParams(intCount))
Case UCase("CORBA_TimeOut")
m_strCORBA_TimeOut = GetValue(arrParams(intCount))
End Select
Next
End Sub
A constructor string is supported in both the HBInterop and the HBProduct components. In the HBInterop component, the constructor string is used to pass a custom database connection string. In HBProduct component, the constructor string is used to pass a TIMEOUT value for the Web Service and the URI for the Web Service listener. Read the installation guide for specific information about how to configure these constructor strings. Creating a COM+ Web ServiceThe HBInterop Web Service was established so that business partners could reuse existing Hanson Brothers systems. In particular, Hanson Brothers wanted to provide access to their catalog and give partners the ability to place orders over the Web. The Web Service supports these needs by exposing methods of the HBProduct and HBInterop DLLs. To provide partners with access to their catalog, we exposed two methods on HBProduct: BrowseCategories and BrowseByCategory. To add these methods to the Web Service, we ran the SDL Wizard that ships with the SOAP Toolkit for Visual Studio 6.0 and selected the option to use the SOAPISAPI filter. The wizard generated the Product.xml file, which contains the SDL. The wizard also generated the Product.sod file, which is what consumers of the Web Service request and it is used by the SOAPISAPI filter to located the SDL file. We also wanted to exposed the Save method of the order class in the HBInterop component. Because the input parameter on the Save method is an ADO recordset, and since SOAP doesn't understand how to process a recordset, we could not directly expose this method. Instead, we created an ASP Listener page that wraps the calls to the Save method. The ASP listener page that wraps the Save method is called Order.asp. It takes a string of XML that contains the order information and converts that XML into an ADO recordset. The recordset is then passed to the Save method of the HBInterop component's Order class. To implement this, we created a SDL file called Order.xml. The SDL defines the input and out parameters of the function call. The first XML fragment below describes the Save method, which expects an input named OrderDetail, defined as a string. The second XML fragment describes the response that has a return parameter called return, defined as long. <element name='Save'> <type> <element name='OrderDetail' type='dt:string'/> </type> </element> <element name='SaveResponse'> <type> <element name='return' type='dt:long'/> </type> </element> After creating the SDL, we created the Order.asp page to process SOAP requests. At the beginning of our Order.asp page we include Listener.asp, which is the generic ASP listener that ships with the SOAP Toolkit for Visual Studio 6.0. <!--#include file="listener.asp"--> Below we define a Save function. The Listener.asp adds logic to call script functions that match the definition contained in the SDL. In this case, our SDL defined a function named Save with a string input parameter, so the listener created a corresponding function in our ASP. Note that because Visual Basic script only supports variants we could not strongly type the parameters. Public Function Save (ByVal Order) Then we began adding logic to the function. The first couple of lines create an instance of the HBInterop Order object, and call the Create method to obtain an empty recordset. Set obj = Server.CreateObject("HBInterop.Order")
Set rs = obj.Create(lngSessionID)
Next we added lines to load the Order string argument , which is expected as XML, into an instance of the DOM. blnReturn = dom.loadXML(Order) Then we needed to add logic for reading data out of the DOM and add it to the recordset. We used XPath syntax and the selectSingleNode method on the DOM to locate specific elements, and then we assigned the node text to the value of the corresponding field. Set node = dom.documentElement.selectSingleNode("//CONTACTNAME")
rs.Fields("ShipAttention").Value = node.Text
After adding the main order information to the recordset, we needed to process the order line items. Because the ORDERLINE element wraps each line item, we used the getElementsByTagName method to obtain access to the collection of ORRDERLINE elements. We set the objNodeList equal to the return of the getElementsByTagName call and then used the length property of objNodelist to determine how many ORDERLINE tags have been supplied. The recordset returned by the Create method is hierarchical and contains a child recordset for order items. To obtain a reference to the child recordset we set rsItems equal to the OrderItems field. Once this reference is established, we can loop through all the ORDERLINE elements and read data into the rsItems recordset. Set objNodeList = dom.documentElement.getElementsByTagName("ORDERLINE")
Set rsItems = rs.Fields("OrderItems").Value
For intCount = 0 To (objNodeList.length - 1)
rsItems.AddNew
Set node = objNodeList.Item(intCount)
rsItems.Fields("OrderNumber").Value = 0
Set node = node.selectSingleNode("//DESCRIPTION")
rsItems.Fields("Name").Value = node.Text
objNodeList.nextNode
Next
After we add all the order information to the recordset, we need to call the Save method of the HBInterop Order class. We make this call by passing in a previously established session ID, which insures that we have established credentials and the populated recordset. Call obj.Save(lngSessionID, rs) Finally, we need to set the ASP function return equal to the OrderNumber field of the recordset. After the Save method successfully executes, the OrderNumber field is populated with the order number returned by SQL Server. Save = rs.Fields("OrderNumber").Value
Calling the COM+ Web ServiceInteropDemo is a Visual Basic project that ships with the HBInterop sample. This sample demonstrates how Hanson Brothers business partners can call the HBInterop Web Service. In all cases, they begin by creating an instance of the proxy object included in the SOAP Toolkit for Visual Studio 6.0 and then they load the SDL. Set objProxy = New ROPE.Proxy objProxy.LoadServicesDescription icURI, 'http://www.HansonBrothers/HBInterop/Order.xml' If they want to call the Order Save method they wrap their order returned by the GetOrderXML function with CDATA and then call the Save method. strReturn = "<![CDATA[" & GetOrderXML & "]]>" strResponse = objProxy.Save(strReturn) Additional ResourcesFor an overview of the SOAP protocol, read Don Box's MSDN Magazine article A Young Person's Guide to The Simple Object Access Protocol. Documentation for the Microsoft SOAP Toolkit for Visual Studio 6.0 is available on this CD. Information on the SOAP specification Additional information on SOAP can be found on the DevelopMentor Web
site HBInterop InstallationFor detailed information on instructions for installing and configuring the HBInterop sample application please read Hanson Brothers Interoperability Sample: Setup. Appendix A XSL for Converting to ADO RecordsetThe following XSL is for transforming XML from an element rich markup to the attribute rich markup expected by ADO: <?xml version="1.0"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl"> <xsl:template match="/"> <xsl:apply-templates select="//Inventory" /> </xsl:template> <xsl:template match="//Inventory"> <xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema"> <s:Schema id="RowsetSchema"> <s:ElementType name="row" content="eltOnly" rs:CommandTimeout="30" rs:updatable="true" rs:ReshapeName="DSRowset1"> <s:AttributeType name="SKU" rs:number="1" rs:nullable="false" rs:writeunknown="true" rs:basecatalog="msinterop" rs:basetable="Product" rs:basecolumn="ID"> <s:datatype dt:type="string" rs:dbtype="str" dt:maxLength="20" rs:maybenull="false" /> </s:AttributeType> <s:AttributeType name="Name" rs:number="2" rs:nullable="false" rs:writeunknown="true" rs:basecatalog="msinterop" rs:basetable="Product" rs:basecolumn="Name"> <s:datatype dt:type="string" rs:dbtype="str" dt:maxLength="50" rs:maybenull="false" /> </s:AttributeType> <s:AttributeType name="Price" rs:number="3" rs:nullable="true" rs:writeunknown="true" rs:basecatalog="msinterop" rs:basetable="Product" rs:basecolumn="Price"> <s:datatype dt:type="i8" rs:dbtype="currency" dt:maxLength="8" rs:precision="19" rs:fixedlength="true" /> </s:AttributeType> <s:AttributeType name="Inventory" rs:number="4" rs:nullable="true" rs:writeunknown="true" rs:basecatalog="msinterop" rs:basetable="Product" rs:basecolumn="Inventory"> <s:datatype dt:type="int" dt:maxLength="4" rs:precision="10" rs:fixedlength="true" rs:maybenull="false" /> </s:AttributeType> <s:extends type="rs:rowbase" /> </s:ElementType> </s:Schema> <rs:data> <xsl:for-each select="Item"> <z:row> <xsl:attribute name="SKU"> <xsl:value-of select="SKU" /> </xsl:attribute> <xsl:attribute name="Name"> <xsl:value-of select="Description" /> </xsl:attribute> <xsl:attribute name="Price"> <xsl:value-of select="Price" /> </xsl:attribute> <xsl:attribute name="Inventory"> <xsl:value-of select="AvailableQuantity"> </xsl:attribute> </z:row> </xsl:for-each> </rs:data> </xml> </xsl:template> </xsl:stylesheet>
|
|||||||||||||||
| © 2000 Microsoft Corporation. All rights reserved. Terms of Use. | |||||||||||||||