Implementing the Web Service

Now that we have our requirements, have defined our model, and have completed the design, it is time to pull it all together into a working Web service. The implementation of a Web service with a workflow can seem a little overwhelming at first. Since each request is nearly its own application, the task of managing it can be complex.

To help with this implementation, we are going to take a somewhat different approach. Traditional Web applications tend to break an application into the different tiers and work on them, much like we did in our design process. Instead of this traditional approach, we will go through a request-by-request build of the Web service. Since we have a design to keep us "rooted" in a common approach, it is safe to take on each request independently without as much concern for deviations among them. We will then address each piece of logic, each interface, and each data element as we encounter them.

We will start by developing the base service itself. This is the container for all of our Web service logic and gives us an initial working prototype to start adding our logic to. Then we will add each of the five requests independently, adding data elements, interfaces, and code as needed. Let's get started.

Note 

If you want to build this Web service, you need to have the HRS system interfaces to base it on. You can write your own according to the specifications I have provided, or you can download the COM object available at http://www.architectingwebservices.com/hrsws/hrs.

Base Service

Since we are going to build the Web service by request functionality, we need to start with the base service. This provides the infrastructure for handling all the requests coming in and the creation of responses sent out. This is analogous to the development of our reservationWS schema document. We started out with a schema that contained the serviceVariables, and we added the interaction schemas one by one.

The base service will need to accomplish some base functionality that will be required of all interactions. This functionality will consist of the following:

  • HRSWS component

  • Listener

  • HRSWS database

  • Session model tables

  • Validation logic

Each of these components will provide functionality that is crucial to our service for each and every request. As we build and test this base service, we should also build a test client that will help by sending valid requests to our Web service. This will provide us an efficient means for testing as we implement each of the five interactions.

HRSWS Component

The HRSWS component contains all the compiled functionality for our Web service. It consists of just one class, clsListener. The only public method in this class is the processRequest function. This method will be called by our listener ASP pages (one each for the embedded and isolated URLs). We will start our implementation by defining this function as shown here:

 Public Function processRequest(ByVal serviceType As Integer, _   ByVal xmlRequest As String, ByVal consumerName As String) As String 

We will have three parameters to our processRequest function: the service type, the request document, and the consumer name. Each listener defines the service type, since it is determined through the URL referenced by the consumer. The request document is the XML data payload passed by the consumer. The consumer name is determined through the client certificate provided by the consumer. The processRequest method returns the XML response in a string format that can be exposed directly to the consumer.

The first thing we need in our function is to reference all the necessary objects to work with the XML. This is essentially the Microsoft XML parser, MSXML. For this service we will work with the Version 4.0 Technical Preview. This is the first version of the parser to support the validation of XML via XML Schemas, so the reasoning behind using a prerelease version is justified.

Let's go ahead and take a look at the initial processRequest function (Listing 7-27) and discuss the interesting parts afterward. Since this is a shell for the function, I have added comments where the business logic will need to be added.

Listing 7-27: The clsListener initial implementation

start example
 Public Function processRequest(ByVal serviceType As Integer,   ByVal xmlRequest As String, ByVal consumerName As String) As String      '—DOM for the request payload      Dim objDomRequest As MSXML2.DOMDocument40      '—DOM for the response payload      Dim objDomResponse As MSXML2.DOMDocument40      '—working DOM for building intermediate documents      Dim objDomWorking As MSXML2.DOMDocument40      '—Variables      Dim blnLoad As Boolean      Dim intSession as Integer      Dim intStage as Integer      '—Database access objects      Dim objConn As ADODB.Connection      Dim objCommand As ADODB.Command      Dim objRS As ADODB.Recordset      '—Create all the XML objects      Set objDomRequest = New MSXML2.DOMDocument40      Set objDomResponse = New MSXML2.DOMDocument40      Set objDomWorking = New MSXML2.DOMDocument40      objDomRequest.async = False      objDomResponse.async = False      objDomWorking.async = False      Set objConn = New Connection      '—Set connection properties      '—connection values defined in global constants for this example      With objConn           .ConnectionTimeout = 25           .Provider = "sqloledb"           .Properties("Data Source").Value = ServerName           .Properties("Initial Catalog").Value = DBName           .Properties("Integrated Security").Value = "SSPI"           .Properties("User ID").Value = UserName           .Properties("password").Value = Password           .Open      End With      Set objCommand = New Command       '—load the xml       objDomRequest.loadXML xmlRequest       '- add validation code here —       '—extract the session and stage data from the service variables       intSession = objDomRequest.selectSingleNode("hrs:reservationWS/         hrs:serviceVariables/sessionID").nodeTypedValue       intStage = objDomRequest.selectSingleNode("hrs:reservationWS/         hrs:serviceVariables/stage").Text       '—Load the base response document template       blnLoad = objDomResponse.Load("http://www.architectingwebservices.com/         interfaces/hrsws/responseTemplate.xml")       if blnLoad then            objDomResponse.selectSingleNode              ("hrs:reservationWS/hrs:serviceVariables/sessionID").nodeTypedValue              = intSession       Else            GoTo Data_Error       End If       '—Adjust intStage if embedded service requested       If serviceType = 1 Then            If intStage = 3 Then intStage = 5            If intStage = 2 Then intStage = 3            If intStage = 1 Then intStage = 2       End If       '—Case statement to handle each request       Select Case intStage       Case 1            '- add request form responder logic here —       Case 2            '- add availability request responder logic here —       Case 3            '- add detail request responder logic here —       Case 4            '- add reservation form responder logic here —       Case 5            '- add reservation request responder logic here —       Case Else       End Select       '—Adjust intStage back for embedded service request       If serviceType = 1 Then             If intStage = 2 Then intStage = 1             If intStage = 3 Then intStage = 2             If intStage = 5 Then intStage = 3       Else             '—append stylesheet reference to payload       End If       '—Set the stage value in the service variables       objDomResponse.selectSingleNode         ("hrs:reservationWS/hrs:serviceVariables/stage").nodeTypedValue = intStage       processRequest = CStr(objDomResponse.xml)       GoTo End_Proc End_Proc:       '—Cleanup       Set objDomRequest = Nothing       Set objDomResponse = Nothing       Set objDomWorking = Nothing       Set objConn = Nothing End Function 
end example

Note 

If you want electronic copies of any code in this book, please go to http://www.architectingwebservices.com. All code there will be compilable and fully functional.

To support all the XML handling, we will define three DOM document objects. These will contain the request, the response, and a working document. The working document will be used to build nodes and temporary documents that will eventually make up our response.

Notice that I set all three document's async property to false. This is necessary if you want to be guaranteed that the data is loaded before continuing in the application. If you immediately start trying to reference this DOM after issuing a load or loadXML request, you will likely get an error because it hasn't completely loaded the data yet.

After extracting the session and stage from the service variables, we also load the response document template. Using a template is an efficient way of getting a jump-start on building an XML document. When you have at least a handful of elements that you know are going to exist and simply need to populate their values, a template approach should be considered. The template for our response document looks like this:

 <hrs:reservationWS xmlns:hrs=" urn:reservationWS">       <hrs:serviceVariables>             <sessionID/>             <stage/>       </hrs:serviceVariables>       <hrs:payload/> </hrs:reservationWS> 

Next, we set the sessionID element value that we referenced from the request. Remember that we have not gotten to any validation of the request coming in yet. We will implement that logic shortly.

The next steps involve the stage mapping that takes the three embedded requests and maps them to the isolated requests. Once this is done we can define the decision tree for each of the five stages through a case statement. Afterward we map the stage back for the embedded requests and set the stage on the response document.

Finally we return the response document and perform some cleanup on our objects. We don't have any error handling included yet because that will be part of the validation logic we will add.

The resulting response is a very simple document that essentially consists of our response document template shown earlier with the sessionID and stage variables set to the values provided in the request. To verify this, we need to develop a listener to send requests to our new class.

Listener

Our listener consists of a couple of ASP pages that essentially collect the request from the consumer, identify the consumer through its client certificate, and make the request of our HRSWS component. I will hold off on the details with the client certificate right now and simply populate that spot with a default value, as shown here:

 dim objHRSRequest dim objDOM set objHRSRequest = Server.CreateObject("HRSWS.clsListener") set objDOM = Server.Createobject("MSXML2.DOMDocument") objdom.async = false objdom.load request Response.ContentType = "text/xml" response.write objHRSRequest.processRequest(2,objdom.xml," consumer") set objdom = nothing set objhrsrequest = nothing 

Normally we could pass the request object directly on, and ASP would do the binary read for us automatically and send the data through the parameter to our class. However, there is a problem with this method when you want to access the request object for other information, such as client certificate data.

The workaround involves loading the request into a DOM and passing in the document manually. We are taking some overhead with this approach, but unfortunately this seems to be the only workaround available when working with XML data sent in binary and utilizing client certificates for authentication.

Note 

Depending on the information you reference, this behavior is either a bug or a feature of ASP's request object. I have no information on whether a fix or update is planned by Microsoft.

This ASP page came from the isolated site so the service type parameter is set to 2. The embedded site's ASP page is identical to this one, except that its service type parameter is set to 1.

Test Client

To test this listener and our service, we cannot simply view it in a typical Web browser. This attempt to contact the ASP page will produce an error because no XML data will be sent. To perform a test, we need to create a custom test client. This can be done using almost any language or platform. You can create your own or follow the Visual Basic design I have here. I will not discuss the technical details of a consumer application until the next chapter, when we start consuming Web services. This is a rudimentary, static client that will serve our purposes for testing only.

To build a test client, start by creating a standard Visual Basic EXE inside of Visual Studio and add three text fields and a button to your form to look roughly like the form shown in Figure 7-23.

click to expand
Figure 7-23: Test client form view

Tip 

Whenever you want to display XML data in a Visual Basic text box (as we are here in the third text box), set the multiline property to true. This allows the data to wrap and indent properly, making it much more readable.

Next, you need to add a reference to the Microsoft XML, V4.0 library. This is the MSXML4 parser, which we will need in our client to handle the XML data sent and returned from the service, as well as the HTTP communication we need to connect. Then, you need to add the code in Listing 7-28 to define your click procedure.

Listing 7-28: HRSWS test consumer subroutine

start example
 Private Sub Command1_Click()      Dim objXMLDoc As MSXML2.DOMDocument40      Dim objXMLNode As MSXML2.IXMLDOMNode      Dim objXMLSend As MSXML2.ServerXMLHTTP40      Dim currNode As String      Dim strServiceURL As String      Set objXMLDoc = New DOMDocument40      Set objXMLSend = New MSXML2.ServerXMLHTTP40      objXMLDoc.async = False      If Text2.Text = "Isolated" Then           Select Case Text1.Text           Case "1"                Text3.text "——————Calling Availability Form———————"                boolTest = objXMLDoc.Load("http://www.architectingwebservices.com/                  interfaces/hrsws/availabilityFormRequest.xml")           Case "2"                Text3.text "—————Calling Availability Process——————"                boolTest = objXMLDoc.Load("http://www.architectingwebservices.com/                  interfaces/hrsws/availabilityRequest.xml")           Case "3"                Text3.text "——————Calling Detail Process————————"                boolTest = objXMLDoc.Load("http://www.architectingwebservices.com/                  interfaces/hrsws/detailRequest.xml")           Case "4"                Text3.text "——————Calling Reservation Form———————"                boolTest = objXMLDoc.Load("http://www.architectingwebservices.com/                  interfaces/hrsws/reservationFormRequest.xml")           Case "5"                Text3.text "——————Calling Reservation Process———————"                boolTest = objXMLDoc.Load("http://www.architectingwebservices.com/                  interfaces/hrsws/reservationRequest.xml")           End Select           strServiceURL = "http://www.architectingwebservices.com/hrsws/isolated" Else           Select Case Text1.Text           Case "1"                Text3.text "——————Calling Availability Process———————"                boolTest = objXMLDoc.Load("http://www.architectingwebservices.com/                  interfaces/hrsws/availabilityRequest.xml")                objXMLDoc.selectSingleNode                  ("hrs:reservationWS/hrs:serviceVariables/stage")                  .nodeTypedValue = "1"           Case "2"                Text3.text "——————Calling Detail Process————————"                boolTest = objXMLDoc.Load("http://www.architectingwebservices.com/                  interfaces/hrsws/detailRequest.xml")                objXMLDoc.selectSingleNode("hrs:reservationWS/hrs:serviceVariables/                  stage").nodeTypedValue = "2"           Case "3"                Text3.text "——————Calling Reservation Process———————"                boolTest = objXMLDoc.Load("http://www.architectingwebservices.com/                  interfaces/hrsws/reservationRequest.xml")                objXMLDoc.selectSingleNode("hrs:reservationWS/hrs:serviceVariables/                  stage").nodeTypedValue = "3"           End Select           strServiceURL = "http://www.architectingwebservices.com/hrsws/embedded"      End If      If boolTest Then           objXMLSend.setOption SXH_OPTION_SELECT_CLIENT_SSL_CERT,             "myCertificate" 'This certificate should match a valid client certificate installed on the server           objXMLSend.open "POST", strServiceURL, False           objXMLSend.send (objXMLDoc.xml)           currNode = objXMLSend.responseText           Text3.text (currNode)      Else           Text3.text "error in load"      End If      Set objXMLDoc = nothing      Set objXMLNode = nothing      Set objXMLSend = nothing End Sub 
end example

In our form we are collecting the stage and the type of service we would like to request. Based on that information, this application references the appropriate request document and submits it to the appropriate listener for the type of service requested. To keep the client simple, I have just created two separate case statements that handle each request accordingly. Every request is handled the exact same way right now by our HRS Web service, so we don't need to get into their details yet.

Now that we have the function for our component defined, a listener that will send it the request, and a means for testing all of these, we can start building out the base service functionality. Like most dynamic applications, this starts with the database behind the process.

Tip 

The XML request files as listed here in the code do exist on the book's Web site, www.architectingwebservices.com. You can either reference these directly in your copy of this code or copy the files down locally on your system and change the paths accordingly. In fact, if you need help troubleshooting your test client, you can point it to the Web service located on this site. Its URLs are http://www.architectingwebservices.com/embedded and http://www.architectingwebservices.com/isolated. You can learn more about this instance of the hotel reservation Web service in the next chapter.

HRSWS Database

It is important to keep in mind that the HRSWS database only contains the data that isn't contained or exposed through the existing HRS system. We are not replicating any data, but either augmenting its functionality or supporting the Web services process itself.

For this service our database will consist of the two models that we defined in our design: the session data model and the content data model. However, for the core service, only the session data model is significant, because it is used by every request made to the Web service.

Session Model

The session data model will contain our customer data along with the sessions and some of their activities. We will use this data to validate the consumer, verify existing sessions and create new sessions, and verify their request precedence. If you recall, the request precedence is important because we have to know that the user makes an availability request prior to making a reservation request.

We have already identified the three tables and their fields in our design, but we have yet to define their data types. Let's go ahead and look at each table's details for our implementation.

The parent table of our entire session model is the consumer table. (See Table 7-2.) Everything is based on it. Without a valid consumer no session can be created and no activity can occur. This table is not meant as a partner contact table, so the data is kept fairly simple. We have a unique identifier, a name for readability, and a certificate field at our disposal. We have yet to determine what value we will actually place in the Consumer_Cert field, but we will implement the entire security model once we build our service.

Table 7-2: Consumers Table Implementation

COLUMN

DATA TYPE

SIZE

NOTES

ConsumerID

int

4

Primary key

Consumer_Name

varchar

50

 

Consumer_Cert

varchar

50

 

The sessions table (Table 7-3) is the direct child of the consumers table. All sessions will exist in this table during their lifespan. This means that the expiration of a session will require the deletion or archival of this session data accordingly (hence the SessionDate column). The unique identifier is an identity field, which will help us to keep up with our session generation. We also have a ConsumerID maintaining integrity with the Consumers table along with a date field for our archival process.

Table 7-3: Sessions Table Implementation

COLUMN

DATA TYPE

SIZE

NOTES

HRSSession

int

4

identity

ConsumerID

int

4

Foreign Key ->Consumers

SessionDate

datetime

8

default is getdate()

The Session_Avail_Checks table will be used to track availability requests made in a session as well as the data provided in the request. This keeps us from having to pass the availability information back and forth with the consumer while the user makes a reservation. Stored in this table, this data will be combined with the data in the reservation request to provide all the information we need to make the reservation request of the HRS system.

The data we will store from the availability request includes the arrival date, the departure date, the number of adults, the number of children, the smoking preference, and any coupons that we used in quoting the price. Table 7-4 also has an HRSSession column to maintain integrity with the Sessions table.

Table 7-4: Session_Avail_Checks Table Implementation

COLUMN

DATA TYPE

SIZE

NOTES

HRSSession

int

4

Foreign key ->Sessions

Check_In_Date

datetime

8

 

Check_Out_Date

datetime

8

 

Number_Adults

tinyint

1

 

Number_Children

tinyint

1

 

Smoking

bit

 

nullable

Coupon

varchar

20

nullable

Now that we have our session data model defined, we need to reference our data. All data access will be done through a stored procedure, so we now need to develop those.

Session Data Access

Our interaction with this data model actually involves two steps. The first is to validate, for each request, whether it is part of an existing session. This simply means that the same user has accessed our service through the same consumer in an established amount of time. For this service, we are setting our session to 30 minutes.

If the session already exists, we simply need to communicate that to our service so that it can validate that fact. If the session does not exist, we need to create a session and then pass its session ID back to our service. To accomplish these two tasks, we will utilize a stored procedure through ADO (ActiveX Data Objects).

Our stored procedure will be called getSessionID (see Listing 7-29.) It will accept as inputs a session ID, a consumer ID, and a found bit (or flag). The session ID serves as an input and an output, since we will pass in any existing session from our service variables and will reference it as the new session ID if necessary. We will know it is necessary because of the found bit. If true, then the session was found, and we can continue processing as requested. If false, then we need to reference the session parameter and make note of the session state of "new" for some processes where it matters.

Listing 7-29: The getSessionID and updateSession stored procedures

start example
 CREATE PROCEDURE dbo.getSessionID(@sessionID int output, @consumerID int,   @found bit output) AS declare @intSession int select @intSession = HRSSession from Sessions where HRSSession = @sessionID   and consumerID = @consumerID if @intSession is NULL begin      insert into sessions(consumerID)Values(@consumerID)      SELECT @sessionID = @@Identity      select @found = 0 end else begin      execute updateSession @intSession      select @sessionID = @intSession      select @found = 1 end GO CREATE PROCEDURE dbo.updateSession(@sessionID int) AS update sessions set SessionDate = getdate() where hrssession = @sessionID GO 
end example

Note 

The updateSession stored procedure is referenced by getSessionID for updating the date of last activity for the session. This will help with the archival of our sessions. Other processes that touch the session could also utilize this stored procedure.

You might question why we need a found parameter if we can simply compare the session ID submitted to the one returned to determine that for ourselves. The reason is the very rare scenario in which a session ID could be submitted that would match the next session ID to be created. This is unlikely to happen, but better safe than sorry!

Finally, we are also passing in the consumer ID as a parameter to simply validate that this consumer is the owner of the session that is referenced. Without this check, consumers could potentially, and successfully, reference the sessions of other consumers in error.

Notice that since we are using an identity field to create our unique identifiers, you need to reference the @@Identity value to properly extract it. This method keeps you from mistakenly grabbing a different session ID that may have been created at the same time.

Caution 

From a security perspective, the fact that we are using an identity field makes you inherently vulnerable to hacker attempts by users on other users in the same consumer. For this to be an actual vulnerability, the consumer would have to expose the session ID to users and allow them to change it on their end. In Chapter 8 I will look at how we should handle this kind of implementation to avoid that risk. Also, as the provider, we can feel comfortable in the fact that at no point are users able to see personal data that they themselves did not enter. Thus, a consecutive request to our Web service will not reveal the personal data that was entered to make a reservation.

processRequest Validation Logic

With our database established and ready to utilize, we can start to build out some of the logic in our function. The next thing we need to do is finish the build out of our listener by adding some validation processing and logic. In our design, we identified three main areas for this validation: well-formedness, schema validation, and data validation. Let's validate the requests coming in taking this approach.

Validate XML Well-formedness

XML well-formedness refers to its syntax legality. This pays attention not to the content in the elements, but simply to the true existence of the elements themselves. Fortunately, our MSXML4 parser will accomplish this task for us by default when we load an XML dataset into its DOM object. In fact, you will have a tough time (perhaps impossible) finding a parser that will allow you to load an illegal XML document into the DOM.

To show how this done, I have listed the code following this paragraph. Notice that I start by setting the async property to False. The MXSML parser is a multithreaded library that by default loads XML data asynchronously, meaning it starts the process and continues processing the remaining steps. For most applications, this is inappropriate because programmers have been taught to create objects only at the last possible moment. This means that once we get around to creating an object, we want to start working with it right away. Unfortunately, if you try to access the DOM as it is still loading, you will likely end up with an error message telling you that whatever you are referencing doesn't exist. Setting the async property to False avoids this issue by requiring the routine to wait for the completion of the load before moving on. Here is the code:

 objDomRequest.async = False blnLoad = objDomRequest.loadXML(xmlRequest) If Not blnLoad Then GoTo Data_Error 

Data_Error is a subroutine that returns a nice message whenever there is an error in the load. It is important to handle your errors very gracefully, and defining different routines for the different errors that can be encountered can help you to do this. Because this is a memory reference, the most likely reason for a failure is that the XML is ill-formed.

The following is a simple routine that provides a user-readable error along with the request package to aid the consumer with troubleshooting the problem:

 Data_Error:     processRequest = "<error><level>critical</level><description>Internal XML Data       Error</description><payload>" & xmlRequest & "</payload></error>"     GoTo End_Proc 

Validate XML Against Schemas

The next step in the validation process is to validate the request against the design schema. We have already developed our schemas. This is how we apply them in our service.

The MSXML4 Technical Preview parser allows us for the first time, to validate XML data against XSD Schemas. Previously, the MSXML parser supported only validation against DTDs and XDRs. Just like with the well-formedness check, you must first load the document into the DOM object.

The schema itself is then loaded into a SchemaCache object. This object allows us to add schemas to it as necessary for validation. After creating the cache object itself, we simply need to reference the schemas along with their namespaces. (See Listing 7-30.) Failing to identify the correct namespaces will either cause validation to not happen at all or, even worse, validate the wrong portion of the document.

Listing 7-30: Schema validation with the MSXML4 parser

start example
 Dim objCache As MSXML2.XMLSchemaCache40 Set objCache = New MSXML2.XMLSchemaCache40 objCache.Add "urn:serviceVariables","http://www.architectingwebservices.com/   interfaces/hrsws/serviceVariables.xsd" Set objDomRequest.schemas = objCache '—Check for failure to load If blnLoad Then      objDomResponse.selectSingleNode        ("hrs:reservationWS/hrs:serviceVariables/sessionID").nodeTypedValue =        intSession Else      GoTo Data_Error End If 
end example

Next we have to set the DOM schema property to the schema cache object. You then have two choices on how to perform the validation itself. You can either set the ValidateOnParse value to True (which validates on loading the data) or call the validate method of the DOM object to manually validate the document in question. The only difference is that in the validation on load, you will not know if it fails due to the legality of the XML syntax or its compliance with the associated schema. For this application we are choosing the latter option, so we are executing the validate method. We are handling the error in the same handler as the other.

Validate Service Variables

Once we have confirmed that the data document can be trusted as legal and valid, we can start referencing its nodes to validate the necessary data. In our initial base functionality, we just need to validate the session data. We saw the stored procedure in the previous section, but now we need to reference it in our code.

We will use the previously established connection, since it is in the same database. We will pass in the sessionID, consumerID, and sessionFound flags. Once they are returned, we will reference the sessionFound and sessionID flags. Adding this code after the request is validated in our processRequest function should produce the desired result. (See Listing 7-31.)

Listing 7-31: Calling getSessionID stored procedure from the processRequest function

start example
 Set objCommand = New Command '—Identify or Create new Session strSession = objDomRequest.selectSingleNode ("hrs:reservationWS/hrs:serviceVariables/sessionID").nodeTypedValue If strSession = "" Then strSession = "0" With objCommand       .ActiveConnection = objConn       .CommandText = "getSessionID"       .CommandType = adCmdStoredProc       .Parameters.Append .CreateParameter("sessionID", adInteger,         adParamInputOutput, , CInt(strSession))       .Parameters.Append .CreateParameter("consumerID", adInteger, adParamInput, ,         501)       .Parameters.Append .CreateParameter("sessionFound", adBoolean,         adParamOutput)       .Execute       intSession = .Parameters("sessionID")       blnNewSession = Not .Parameters("sessionFound") End With 
end example

Note 

If you are not familiar with using the ADO Command object, you may prefer using a Recordset option. If you are not familiar with ADO at all, I recommend that you get an ADO reference book such as ADO Examples and Best Practices by William Vaughn.

You might notice that we are extracting the session value as a string even though it is an integer data type. The reason for this is that the session could be null if it is the first request. Setting an integer to the null value will give you an error, and rather than handle an error, it is easier to extract it as a string. This is even further justified in this situation because we are validating it through our getSessionID stored procedure, which can handle invalid session IDs. This allows us to set the session to 0 if it is empty and call our procedure just as if we had received a session ID in the request. I did not need to take this same approach with the stage value because the schema defines that value as required.

At the end you see where we are saving the session ID to a local variable and setting a local flag for the session state for efficiency's sake. So, regardless of whether we are in a new or an existing session, we are done with our base session validation. Each interaction stage will also have its own validation, but that process is different for each request and can simply reference the local variables we have assigned.

At this point, if you make a request of the Web service, you will end up getting very much a skeleton form of what the response should be as shown here:

 <?xml version="1.0"?> <hrs:reservationWS xmlns:hrs="urn:reservationWS">       <hrs:serviceVariables>             <sessionID>1210</sessionID>             <stage>1</stage>       </hrs:serviceVariables>       <hrs:payload/> </hrs:reservationWS> 

We basically are getting the valid session ID and the stage of our request. We have yet to add any of the logic to populate our payload according to the request that was made. However, we have laid the groundwork now to add that functionality, so we will start doing so.

Availability Form

The availability form is the first request in our isolated service model. Based on sheer data volume, this will be a light request, heavy response call. Fortunately, this is a fairly simple response to assemble because the response payload is always the same for every request.

The payload refers to the content of the response, not the service variables. The service variables will never be static, since they must be validated and potentially modified or created. Fortunately, our base service takes care of that for every request already, so all we are left with is a simple inclusion of static data in the response skeleton we have already created. What we need to do then is add the responder logic and unit test our response.

processRequest Logic

The logic for our availability form will fall into our group of case statements in the middle of our current processRequest function. We will specifically be dealing with the case in which our intStage value is 1, since that is the stage of our availability form request. The code for our responder logic is shown in Listing 7-32.

Listing 7-32: Availability form responder logic

start example
 Case 1      '—Load the availability form      objDomWorking.Load ("http://www.architectingwebservices.com/        interfaces/hrsws/availabilityFormResponse.xml")      '—Load the response into the Response Node      Set objNodeWorkingList = objDomWorking.selectNodes        ("hrs:availabilityFormInteraction")      Set objResponseNode = objNodeWorkingList.Item(0)      Set objNodeWorkingList = Nothing 
end example

The first thing we have to do is load the availabilityFormResponse XML document that we defined in the "Isolated Service Interactions" section of our design process. This document is then loaded into our working node list so that we can transfer the document into a node. We have to do this because we cannot take one XML document and append it to another. Instead, we must transfer the document to be embedded into a node element. The difference here is somewhat semantic, since any node can be considered a legal document. The problem is that you can only load a document and can only "move" a node, so this intermediate transfer step is necessary.

Finally, we clean up our working list object, and we are done. The only other necessary step is to add our stylesheet reference, since this is an isolated service request, but we will add that functionality globally after we complete all the individual responses.

Sample Response

The response to a stage 1 request of your isolated service should now look like Listing 7-33.

Listing 7-33: Availability form sample response

start example
 <?xml version="1.0"?> <hrs:reservationWS xmlns:hrs=" urn:reservationWS">       <hrs:serviceVariables>             <sessionID>1210</sessionID>             <stage>1</stage>       </hrs:serviceVariables>       <hrs:payload>             <hrs:hotelAvailabilityForm>                   <checkInDate>                         <name>checkInDate</name>                         <caption>Check-In Date</caption>                         <checkInMonth maxValues="1">                               <name>checkInMonth</name>                               <option value="1" caption=" Jan"/>                               ...                               <option value="12" caption=" Dec"/>                         </checkInMonth>                         <checkInDay maxValues="1">                               <name>checkInDay</name>                               <option value="1" caption="1"/>                               ...                               <option value="31" caption="31"/>                         </checkInDay>                         <checkInYear maxValues="1">                               <name>checkInYear</name>                               <option value="2001" caption="2001"/>                               <option value="2002" caption="2002"/>                         </checkInYear>                   </checkInDate>                   <checkOutDate>                         <name>checkOutDate</name>                         <caption>Check-Out Date</caption>                         <checkOutMonth maxValues="1">                               <name>checkInMonth</name>                               <option value="1" caption=" Jan"/>                               ...                               <option value="12" caption=" Dec"/>                         </checkOutMonth>                         <checkOutDay maxValues="1">                               <name>checkInDay</name>                               <option value="1" caption="1"/>                               ...                               <option value="31" caption="31"/>                         </checkOutDay>                         <checkOutYear maxValues="1">                               <name>checkInYear</name>                               <option value="2001" caption="2001"/>                               <option value="2002" caption="2002"/>                         </checkOutYear>                   </checkOutDate>                   <numberAdults maxValues="1">                         <name>numberAdults</name>                         <caption>Number of Adults</caption>                         <option selected=" TRUE" value="1" caption="1"/>                         <option value="2" caption="2"/>                         <option value="3" caption="3"/>                         <option value="4" caption="4"/>                   </numberAdults>                   <numberChildren maxValues="1">                         <name>numberChildren</name>                         <caption>Number of Children</caption>                         <option selected=" TRUE" value="0" caption="0"/>                         <option value="1" caption="1"/>                         <option value="2" caption="2"/>                         <option value="3" caption="3"/>                         <option value="4" caption="4"/>                   </numberChildren>                   <city>                         <name>city</name>                         <caption>City</caption>                         <maxLength>30</maxLength>                   </city>                   <state>                         <name>state</name>                         <caption>State</caption>                         <maxLength>2</maxLength>                         <minLength>2</minLength>                   </state>                   <landmark>                         <name>landmark</name>                         <caption>Landmark</caption>                         <maxLength>50</maxLength>                   </landmark>                   <approxPrice>                         <name>approxPrice</name>                         <caption>Price Preference</caption>                         <maxLength>4</maxLength>                   </approxPrice>                   <smoking maxValues="1">                         <name>smoking</name>                         <caption>Smoking</caption>                         <option value=" TRUE" caption=" Yes"/>                         <option value=" FALSE" caption=" No"/>                   </smoking>                   <couponCode>                         <name>couponCode</name>                         <caption>Coupon Code</caption>                         <maxLength>10</maxLength>                   </couponCode>                   <bedSize maxValues="1">                         <name>bedSize</name>                         <caption>Bed Size</caption>                         <option value=" King" caption=" King"/>                         <option value=" Queen" caption=" Queen"/>                         <option value=" Double" caption=" Double"/>                   </bedSize>                   <hotelAmenities maxValues="*">                         <name>amenity</name>                         <caption>Hotel Amenities</caption>                         <option value=" Concierge" caption=" Concierge"/>                         <option value=" Pool" caption=" Pool"/>                         <option value=" Workout Facilities" caption=" Workout                           Facilities"/>                         <option value="24-hour Room Service" caption="24-hour Room                           Service"/>                         <option value=" Restaurant" caption=" Restaurant"/>                         <option value=" High Speed Internet Access" caption=" High                           Speed Internet Access"/>                   </hotelAmenities>             </hrs:hotelAvailabilityForm>       </hrs:payload> </hrs:reservationWS> 
end example

The only difference between your response and this one might be the session ID itself for reasons that should be obvious by now. All other requests will still have only the simple response that is provided by our base service.

That is it for the availability form response. This may seem elementary, and it should. Very little logic is required for this response. However, the availability request is much more involved, as we will now find out.

Availability Request

This request is one of the most challenging because it touches several external systems. We will need to interact with the session model in our database and access two interfaces of the HRS system. We will start by developing the logic for working with our session data.

Data Logic

We need to reference our data model to create or update the session data for each availability request made of the system. The only table we need to touch for this purpose is the Session_Avail_Checks table we developed earlier. This table contains the pertinent information for each availability check so that it can be referenced when a reservation is made. This information consists of the following:

  • Check-in date

  • Check-out date

  • Number of adults

  • Number of children

  • Smoking preference

  • Coupon

We now just need to determine whether to perform an insert or an update of this information. This data has a one-to-one relationship with the sessions table, so a matching session must already exist for you to enter a record in this table. At this point in the process, we do not need to concern ourselves with whether the session exists because that has already been resolved by the base service logic. We even have the session ID already available to you.

We also have the session state available to us but it won't help us with this process. That flag tells us if this is a new session, which means that you would need to create the record in the Session_Avail_Checks table, but the fact that it is an existing session does not necessarily mean that a previous availability request has been made. The user may have simply made an availability form request.

This means that we need the database to logically determine whether this data should be inserted or updated in the table. Since we are accessing all of our data through stored procedures, it makes sense to go ahead and embed this there and externalize it to our service logic. Let's go ahead and create the stored procedure and name it setAvailabilityCriteria. We will pass it the session ID as well as the data from our preceding list as shown in Listing 7-34.

Listing 7-34: setAvailabilityCriteria stored procedure

start example
 CREATE PROCEDURE dbo.setAvailabilityCriteria(@sessionID int,      @checkInDate datetime, @checkOutDate datetime,      @numberAdults int, @numberChild int, @coupon varchar(10), @smoke bit)      AS declare @intSession int select @intSession = hrssession from session_avail_checks where HRSSession = @sessionID if @intSession is NULL      insert into session_avail_checks (hrssession,check_in_date,check_out_date,        number_adults,number_children,coupon,smoking)      Values(@sessionID,@checkindate,@checkoutdate,        @numberadults,@numberChild,@coupon,@smoke) else      update session_avail_checks set           hrssession=@sessionID,           check_in_date=@checkindate,           check_out_date=@checkoutdate,           number_adults=@numberadults,           number_children=@numberChild,           coupon=@coupon,           smoking=@smoke GO 
end example

This is a relatively simple stored procedure that takes the data and determines whether to execute an insert or update and then does so. You might have noticed that the @smoke parameter, representing users' smoking preference, is defined as a bit here. It was typed as a Boolean in our XML schema, but the two data types are equivalents. Let's go ahead and develop the business logic for our HRS system calls.

wsFetchAvailability Logic

The business logic is typically nonexistent in Web services because they tend to feed off of existing systems and logic. That becomes obvious in dealing with the HRS system for our reservation Web service. Everything between the listener and the HRS system deals with packaging and formatting the data it shuttles between the consumer and our system. I call this section the HRS interface logic because that is what it is.

We determined earlier that we would need two calls to the HRS system to make an availability request and build the appropriate response. To do this we are going to create a single private method that contains all this logic, called wsFetchAvailability. We have already designed its interface, but we need to build out its functionality. The functionality essentially consists of just four steps:

  • Call HRS availability routine

  • Build results XML document

  • Call HRS hotel details routine

  • Provide results

We need to assemble the request to the availability routine to get any available matches. The data is passed from the responder just as we need it to make the request, so that step is relatively simple, as shown here:

 varResult = objHRSavailability.fetchAvailability(dtmCheck_In, dtmCheck_Out,   intAdults, intChildren, strCity, strState, strLandmark, strCoupon, curPrice,   strBed, blnSmoking, varAmenities) 

What we get back is a variant two-dimensional array that contains all the matching hotels returned by the HRS system. This array contains the following data:

  • Hotel ID

  • Proximity to specified location

  • Room type

  • Room rate

We need to then take this data and assemble it in an XML format for our response. Since this is the first time we have looked at creating nodes in the DOM, let's take a closer look at this process.

There are actually two steps to creating a node in the DOM. The first is the creation of a node object, which has to be based on an existing document, as seen here:

 Set objResponseNode = objDomResponse.createElement("hotel") 

I'm not sure what, if any, relationship there is to the document it was created from because this step does not modify the DOM in any way. To then add this node to the document, we need to utilize the appendChild method of the DOM, specifying the parent for the node we want to add, as shown here:

 objDomResponse.selectSingleNode("availabilityResults").appendChild   objResponseNode 

These two steps actually add an element node named hotel to the availabilityResults node in the root of the objDomResponse document. It is very simple to add a node when you know the path where you want it and that path is valid. Unfortunately this will not always be the case, like when you are building a dynamic dataset.

This situation actually occurs in this function because we do not know how many hotels may be returned from the availability search. The information comes back in an array for this reason. If we have three matching hotels returned from the HRS system, how do we specify the path for adding the hotel details? This dilemma is shown in the following XML data set:

 <availabilityResults>       <hotel>             <hotelID>1234</hotelID>             ...       </hotel>       <hotel>             <hotelID>1235</hotelID>             ...       </hotel> </availabilityResults> 

If we want to add the room type to the second hotel listing, how do we do that? If we list the path availabilityResults/hotel in any reference in the document, it references the first instance. We can build an involved algorithm in which we keep track of the hotel IDs for each instance and then search for the proper parent based on the value of that child element. Of course, I'd rather not!

The approach I would rather take when building a sequence of like nodes is to implement the logic such that I am always working with the last child of that sequence. That means completing one instance of the hotel element before moving on to the next. Then we can reference the "active" instance with the following code reliably:

 objDomResponse.selectSingleNode("availabilityResults").lastChild.appendChild   objResponseNode 

In keeping with this approach, we are calling the HRS system's fetchHotel routine while we are building the hotel node:

 varDetails = objHRSavailability.fetchHotel(varResult(x, 0)) 

This routine returns us the following data in a sequenced array:

  • Hotel name

  • Manager

  • Address

  • City

  • State

  • Phone number

We don't care about the manager, so we will ignore that data. The rest we will utilize in our response. Putting all this together, we end up with the function in Listing 7-35.

Listing 7-35: The wsFetchAvailability method

start example
 Private Function wsFetchAvailability(_   ByVal dtmCheck_In As Date, _   ByVal dtmCheck_Out As Date, _   ByVal intAdults As Integer, _   ByVal intChildren As Integer, _   ByVal strCity As String, _   ByVal strState As String, _   ByVal strLandmark As String, _   ByVal strCoupon As String, _   ByVal curPrice As Currency, _   ByVal strBed As String, _   ByVal blnSmoking As Boolean, _   ByVal varAmenities As Variant) As String      Dim objDomResponse As MSXML2.DOMDocument40      Dim objResponseNode As MSXML2.IXMLDOMNode      Dim root As IXMLDOMElement      Dim objHRSavailability As reservationSystem.clsHRS      Dim varResult As Variant      Dim varDetails As Variant      Dim currElement As Variant      Dim hotelID As Integer      Dim x As Integer      On Error GoTo End_Proc      Set objDomResponse = New DOMDocument      objDomResponse.async = False      Set objHRSavailability = New clsHRS      objDomResponse.loadXML ("<availabilityResults/>")      '—call fetchavailability      varResult = objHRSavailability.fetchAvailability(dtmCheck_In, dtmCheck_Out,        intAdults, intChildren, strCity, strState, strLandmark, strCoupon,        curPrice,strBed, blnSmoking, varAmenities)      '—parse through results to get hotelID's      Dim counter As Integer      Set root = objDomResponse.documentElement      '—navigating the resulting array that contains 4x records for x matches      For x = 0 To UBound(varResult)      '—create a new hotel node      Set objResponseNode = objDomResponse.createElement("hotel")      objDomResponse.selectSingleNode("availabilityResults").appendChild        objResponseNode      Set objResponseNode = objDomResponse.createElement("hotelID")      objResponseNode.nodeTypedValue = varResult(x, 0)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .appendChild objResponseNode      Set objResponseNode = objDomResponse.createElement("proximity")      objResponseNode.nodeTypedValue = varResult(x, 1)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .appendChild objResponseNode      Set objResponseNode = objDomResponse.createElement("roomType")      objResponseNode.nodeTypedValue = varResult(x, 2)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .appendChild objResponseNode      Set objResponseNode = objDomResponse.createElement("roomRate")      objResponseNode.nodeTypedValue = varResult(x, 3)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .appendChild objResponseNode      '—call fetchhotel      varDetails = objHRSavailability.fetchHotel(varResult(x, 0))      '—Navigate hotel details return data      Set objResponseNode = objDomResponse.createElement("hotelName")      objResponseNode.nodeTypedValue = varDetails(0)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .appendChild objResponseNode      Set objResponseNode = objDomResponse.createElement("hotelPhone")      objResponseNode.nodeTypedValue = varDetails(5)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .appendChild objResponseNode      Set objResponseNode = objDomResponse.createElement("hotelAddress")      objDomResponse.selectSingleNode("availabilityResults").lastChild        .appendChild objResponseNode      '—Build the hotel address element      Set objResponseNode = objDomResponse.createElement("address")      objResponseNode.nodeTypedValue = varDetails(2)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .selectSingleNode("hotelAddress").appendChild objResponseNode      Set objResponseNode = objDomResponse.createElement("city")      objResponseNode.nodeTypedValue = varDetails(3)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .selectSingleNode("hotelAddress").appendChild objResponseNode      Set objResponseNode = objDomResponse.createElement("state")      objResponseNode.nodeTypedValue = varDetails(4)      objDomResponse.selectSingleNode("availabilityResults").lastChild        .selectSingleNode("hotelAddress").appendChild objResponseNode      Next      wsFetchAvailability = objDomResponse.xml End_Proc:      Set objDomResponse = Nothing      Set objResponseNode = Nothing      Set root = Nothing      Set objHRSavailability = Nothing End Function 
end example

With the wsFetchAvailability method developed, we now need to integrate it into our responder logic.

processRequest Logic

Just like with the availability form request, we have a placeholder in the processRequest function for the availability request. In the case statement in which intStage=2 we will add the responder logic for this request.

To call the wsFetchAvailability method, we have to parse out the XML data into local variables. You could theoretically reference the DOM elements directly in the method call, but that gets very unwieldy with 12 elements with one variant array and one optional value.

One way to handle a mixture of optional and required nodes is to borrow a concept from the SAX approach to parsing. In the DOM model, we can traverse the nodes and capture each node value as it is encountered. That keeps you from having to handle errors when attempting to access nonexistent nodes. Also, since we are validating the request through the schema, we don't need to check that the required nodes are present.

I implement this approach by creating a node list out of the request document, as shown here:

 Set objNodeWorkingList = objDomRequest.selectNodes("hrs:reservationWS/ hrs:payload/hrs:availabilityInteraction/availabilityRequest") Set objNodeWorking = objNodeWorkingList.Item(0) 

After creating my list of nodes, I find the number of nodes through the length property and walk one child at a time through a For loop. A Case statement then works well for "capturing" the nodes as they are encountered, as shown here:

 intLen = objNodeWorking.childNodes.length For x = 0 To intLen - 1      Select Case objNodeWorking.childNodes(x).nodeName      Case "checkInDate"      ...      End Select Next 

In most of the cases I am using a simple text reference to save the value to a local variable:

 strCheckIn = CStr(objNodeWorking.selectSingleNode("checkInDate").Text) 

The only exceptions to this example are situations in which the child nodes have child nodes of their own. This occurs in two occasions in this request: in the locale and preferences elements. In the case of the locale node, we need to come up with an algorithm for determining which of the city, state, and landmark nodes are present. The business rule is that either the city and state must be present or the landmark must be present. However, both nodes may also be present, so we need to account for that as well. I used a series of If...Then statements to perform this check, as you will see in the code.

That takes care of extracting the data to build the parameter list to send to the wsFetchAvailability function. Before making that call, we need to log the request with our database. This is done through the setAvailabilityCriteria stored procedure we created earlier. We will reuse our connection and redefine our command object to call our procedure.

After that is successful, we can then make our call to wsFetchAvailability. Since we are getting back an XML document, we simply need to add the root node for our interaction and return it. It is probably a good idea to validate this document just to make sure your routine did not generate any inconsistencies with your schema. Listing 7-36 shows the responder logic in its entirety.

Listing 7-36: Availability request responder logic

start example
 Case 2      '—call availability process      Set objNodeWorkingList = objDomRequest.selectNodes        ("hrs:reservationWS/hrs:payload/hrs:availabilityInteraction/        availabilityRequest")      Set objNodeWorking = objNodeWorkingList.Item(0)      intLen = objNodeWorking.childNodes.length      For x = 0 To intLen - 1           Select Case objNodeWorking.childNodes(x).nodeName           Case "checkInDate"                strCheckIn = CStr(objNodeWorking.selectSingleNode("checkInDate")                .Text)           Case "checkOutDate"                strCheckOut = CStr(objNodeWorking.selectSingleNode                ("checkOutDate").Text)           Case "numberAdults"                strAdults = CStr(objNodeWorking.selectSingleNode("numberAdults")                .Text)           Case "numberChildren"                strChild = CStr(objNodeWorking.selectSingleNode("numberChildren")                .Text)           Case "coupon"                strCoupon = CStr(objNodeWorking.selectSingleNode("coupon")                .Text)           Case "approxPrice"                strPrice = CStr(objNodeWorking.selectSingleNode("approxPrice")                .Text)           Case "locale"                If objNodeWorking.childNodes(x).firstChild.nodeName =                "cityState" Then                   strCity = CStr(objNodeWorking.childNodes(x).firstChild                     .selectSingleNode("city").Text)                   strState = CStr(objNodeWorking.childNodes(x).firstChild                     .selectSingleNode("state").Text)                   If objNodeWorking.childNodes(x).lastChild.nodeName =                     "landmark" Then                       strLandmark = CStr(objNodeWorking.childNodes(x).lastChild                          .selectSingleNode("landmark").Text)                      End If                Else                      strLandmark = CStr(objNodeWorking.childNodes(x).firstChild                        .selectSingleNode("landmark").Text)                      If objNodeWorking.childNodes(x).lastChild.nodeName                        = "cityState" Then                           strCity = CStr(objNodeWorking.childNodes(x).lastChild                             .selectSingleNode("city").Text)                           strState = CStr(objNodeWorking.childNodes(x).lastChild                             .selectSingleNode("state").Text)                      End If                End If           Case "preferences"                intLen2 = objNodeWorking.selectSingleNode("preferences").childNodes                   .length                z = 0                For y = 0 To intLen2 - 1                     Select Case objNodeWorking.selectSingleNode("preferences")                       .childNodes(y).nodeName                     Case "bedSize"                          strBed = CStr(objNodeWorking.selectSingleNode                            ("preferences").selectSingleNode("bedSize").Text)                     Case "smoking"                          strSmoke = CStr(objNodeWorking.selectSingleNode                            ("preferences").selectSingleNode("smoking").Text)                     Case "amenity"                          varAmenities(z) = CStr(objNodeWorking                            .selectSingleNode("preferences").childNodes(y).Text)                          z = z + 1                     End Select                Next           Case Else           End Select      Next      '—Enter selected criteria into database      Set objCommand = New Command      With objCommand           .ActiveConnection = objConn           .CommandText = "setAvailabilityCriteria"           .CommandType = adCmdStoredProc           .Parameters.Append .CreateParameter("sessionID", adInteger,             adParamInput, , CInt(strSession))           .Parameters.Append .CreateParameter("checkInDate", adDate, adParamInput             ,,CDate(strCheckIn))           .Parameters.Append .CreateParameter("checkOutDate", adDate,             adParamInput, , CDate(strCheckOut))           .Parameters.Append .CreateParameter("numberAdults", adInteger,             adParamInput, , CInt(strAdults))           .Parameters.Append .CreateParameter("numberChildren", adInteger,             adParamInput, , CInt(strChild))           .Parameters.Append .CreateParameter("coupon", adChar, adParamInput, 10,             strCoupon)           .Parameters.Append .CreateParameter("smoking", adBoolean, adParamInput             ,,CBool(strSmoke))           .Execute      End With      '—Call wsFetchAvailability      strPayload = wsFetchAvailability(CDate(strCheckIn), _        CDate(strCheckOut), _        CInt(strAdults), _        CInt(strChild), _        strCity, _        strState, _        strLandmark, _        strCoupon, _        CCur(strPrice), _        strBed, _        CBool(strSmoke), _        varAmenities)      'Add root node for availability interaction      strPayload = "<hrs:availabilityInteraction xmlns:hrs="" urn:        availabilityInteraction"">" & strPayload & "</hrs:availabilityInteraction>"      '—Load the availability results into a DOM and validate      objDomWorking.loadXML (strPayload)      blnLoad = objDomWorking.Validate      If Not blnLoad Then GoTo Data_Error      '—Load the response into the Response Node      Set objNodeWorkingList = objDomWorking.selectNodes        ("hrs:availabilityInteraction")      Set objResponseNode = objNodeWorkingList.Item(0)      Set objNodeWorkingList = Nothing 
end example

Sample Response

With that complete, we should next run a unit test of this interaction by making the request with our test client. Listing 7-37 shows a valid response that we might get.

Listing 7-37: Availability request response

start example
 <?xml version="1.0"?> <hrs:reservationWS xmlns:hrs="urn:reservationWS">       <hrs:serviceVariables>             <sessionID>1086</sessionID>             <stage>2</stage>       </hrs:serviceVariables>       <hrs:payload>             <hrs:availabilityInteraction xmlns:hrs=" urn:availabilityInteraction">                   <availabilityResults>                         <hotel>                               <hotelID>543</hotelID>                               <proximity>1.5</proximity>                               <roomType>Suite</roomType>                               <roomRate>69.99</roomRate>                               <hotelName>Plano West</hotelName>                               <hotelPhone>972-555-1212</hotelPhone>                               <hotelAddress>                                     <address>555 Main Street</address>                                     <city>Plano</city>                                     <state>TX</state>                               </hotelAddress>                         </hotel>                         <hotel>                               <hotelID>12345</hotelID>                               <proximity>3</proximity>                               <roomType>Economy</roomType>                               <roomRate>49.99</roomRate>                               <hotelName>Plano West</hotelName>                               <hotelPhone>972-555-1212</hotelPhone>                               <hotelAddress>                                     <address>555 Main Street</address>                                     <city>Plano</city>                                     <state>TX</state>                               </hotelAddress>                         </hotel>                   </availabilityResults>             </hrs:availabilityInteraction>       </hrs:payload> </hrs:reservationWS> 
end example

If you are following along and creating your own version of this Web service, your results will likely be different, but your structure should still be the same.

Hotel Detail View

This is the interaction for which we have to provide all the logic and data. The form requests we have to support are simply referencing XML documents and embedding them, so this one will be a little more challenging.

In this request, the consumer is requesting more information on one of our hotels. The idea is that this request will follow an availability request, but we are not enforcing such a rule. Theoretically, the consumer could request this information for any reason and we would provide it. This makes the process a little easier on us.

If we refer to the Hotel Detail View section in our design, we'll see that we have to build these three entities to provide this functionality:

  • Content data model

  • Stored procedure

  • wsFetchHotelContent function

Content Data Model

The content data model has already been designed, but we have not specified the field types and details. We need to start there before doing anything else.

The data model consists of two tables: Hotel_Content and Content_Ref_Table. The Content_Ref_Table (Table 7-5) acts as a reference table for the actual content stored in Hotel_Content (Table 7-6), as we saw in the design of the data model.

Table 7-5: Content_Ref_Table Implementation

COLUMN

DATA TYPE

SIZE

NOTES

ContentRef_Key

tinyint

1

Primary key

Ref_Name

varchar

20

 
Table 7-6: Hotel_Content Implementation

COLUMN

DATA TYPE

SIZE

NOTES

HotelID

int

4

Primary key

ContentRef_Key

tinyint

1

Foreign key -> Content_Ref_Table

Ref_Value

varchar

255

 

We have some predefined content that we want to expose immediately, so we should populate the Content_Ref_Table with that data. The four content types are shown in Table 7-7.

Table 7-7: Initial Data Values in Content_Ref_Table

CONTENT_REF_KEY

REF_NAME

1

mainPicture

2

facilitiesPicture

3

roomPicture

4

hotelDescription

I have already discussed the relationship between these two tables, so let's move on to exposing this data to our service.

Data Logic

We eventually need to expose this data in XML, so typically you might consider using SQL Server 2000's "for XML" functionality. Unfortunately, our database model is set up so that the element name must be built dynamically (the Ref_Name field), and that is something SQL Server still cannot do. Instead, we have to extract the data through the usual recordset object and build the XML manually in our program logic.

Like I said, we are building the XML dynamically by retrieving the element name and values that relate to each other for a given hotel ID. That means that we have an integer input parameter and will return a series of records containing the content. It is fairly straightforward when you look at it in Listing 7-38:

Listing 7-38: The getHotelContent stored procedure

start example
 CREATE PROCEDURE getHotelContent(@hotelID int) AS select Ref_Name, Ref_Value from Hotel_Content join Content_Ref_Table on Hotel_Content.ContentRef_Key =   Content_Ref_Table.ContentRef_Key where hotelID = @hotelID GO 
end example

We are now ready to move on to the code that will access our data in the content data model.

wsFetchHotelContent Logic

This logic was turned into a function because it really represents a unit of work that could easily be utilized by other processes as either an extension to our Web service or possibly even other applications and services.

This function takes a hotel ID number, extracts the content data from our database, and formats it in XML for the response. This function builds the XML manually. This is not something you want to do regularly, but when the data is dynamic and yet clean enough for XML (as ours was designed), then this approach can make sense.

There is nothing very special about this function or logic, so I'll just present it in Listing 7-39 for your review.

Listing 7-39: The wsFetchHotelContent function

start example
 Public Function wsFetchHotelContent(ByVal Hotel_ID As Integer) As String      Dim objDomResponse As MSXML2.DOMDocument40      '—Database access objects      Dim objConn As ADODB.Connection      Dim objCommand As ADODB.Command      Dim objRecordset As ADODB.Recordset      Dim strResponse As String      Dim strDomain As String      Dim strPayload as String      '—If an error is encountered, just return an empty string      On Error GoTo End_Proc      Set objConn = New Connection      '—Set connection properties.      With objConn           .ConnectionTimeout = 25           .Provider = "sqloledb"           .Properties("Data Source").Value = ServerName           .Properties("Initial Catalog").Value = DBName           .Properties("Integrated Security").Value = "SSPI"           .Properties("User ID").Value = UserName           .Properties("password").Value = Password           .Open      End With      Set objCommand = New Command      Set objCommand.ActiveConnection = objConn      objCommand.CommandText = "getHotelContent"& Hotel_ID      Set objRecordset = New Recordset      objRecordset.Open objCommand      objRecordset.MoveFirst      '—Dynamically build the XML response from the recordset returned      strResponse = "<hotelContent>"      strDomain = "http://www.architectingwebservices.com"      Do while Not objRecordset.EOF           If objRecordset(0) <> "Hotel Description" then                strPayload = strDomain & objRecordset(1)           Else                strPayload = strDomain & objRecordset(1)           End If           strResponse = strResponse & "<" & objRecordset(0) & ">" & strPayload &           "</" & objRecordset(0) & ">"           objRecordset.MoveNext      Loop      strResponse = strResponse & "</hotelContent>"      wsFetchHotelContent = strResponse End_Proc:      '—cleanup      objRecordset.Close      objConn.Close      Set objDomResponse = Nothing      Set objConn = Nothing      Set objCommand = Nothing      Set objRecordset = Nothing End Function 
end example

processRequest Logic

Now we need to integrate our function into the responder logic in the processRequest function. This is less involved simply because we are only passing one piece of information to it, the hotel ID. This eliminates the need for a lot of parsing. In fact, we can actually reference the element in the request DOM directly in our function call.

Like the wsFetchHotelContent function itself, we will be adding a parent node to its response manually to keep it simple. This makes it a valid detailInteraction document, which is loaded into the objResponseNode object, as shown in Listing 7-40.

Listing 7-40: Hotel detail responder logic

start example
 Case 3      '—call detail process      strPayload = wsFetchHotelContent(objDomRequest.selectSingleNode        ("hrs:reservationWS/hrs:payload/detailInteraction/detailRequest/hotelID")      .nodeTypedValue)      strPayload = "<hrs:detailInteraction xmlns:hrs="" urn:detailInteraction"">" &        strPayload & "</hrs:detailInteraction>"      '—Load the availability results into a DOM      blnLoad = objDomWorking.loadXML(strPayload)      If Not blnLoad Then GoTo Data_Error      '—Load the response into the Response Node      Set objNodeWorkingList = objDomWorking.selectNodes("hrs:detailInteraction")      Set objResponseNode = objNodeWorkingList.Item(0)      Set objNodeWorkingList = Nothing 
end example

Sample Response

Once the database is populated with information on the hotels, you should be able to make a request through the test client that produces something similar to the document in Listing 7-41.

Listing 7-41: Hotel detail request response

start example
 <?xml version="1.0"?> <hrs:reservationWS xmlns:hrs=" urn:reservationWS">       <hrs:serviceVariables>             <sessionID>1218</sessionID>             <stage>3</stage>       </hrs:serviceVariables>       <hrs:payload>             <hrs:detailInteraction xmlns:hrs=" urn:detailInteraction">                   <hotelContent>                         <mainPicture>http://www.architectingwebservices.com/hrsws/                           images/dfw/dwntwn/front.gif</mainPicture>                         <facilitiesPicture>http://www.architectingwebservices.com/                           hrsws/images/dfw/dwntwn/banquethall.gif</facilitiesPicture>                         <facilitiesPicture>http://www.architectingwebservices.com/                           hrsws/images/dfw/dwntwn/pool.gif</facilitiesPicture>                         <roomPicture>http://www.architectingwebservices.com/hrsws/                           images/dfw/dwntwn/economy.gif</roomPicture>                         <roomPicture>http://www.architectingwebservices.com/hrsws/                           images/dfw/dwntwn/suite.gif</roomPicture>                         <hotelDescription>Our four-star resort is close to downtown                           with easy access to all of DFW's major attractions.                           Recently renovated with all the amenities a business                           traveler needs to stay productive while on the                           road.</hotelDescription>                   </hotelContent>             </hrs:detailInteraction>       </hrs:payload> </hrs:reservationWS> 
end example

This completes the hotel detail step of our hotel reservation Web service. This functionality is a significant example of how it is possible to augment an existing process that wasn't originally designed for use on the Internet. This hopefully gives you confidence that such an approach can be taken when dealing with processes on legacy systems.

The next request we need to build out is the reservation form. This will be the first step for isolated service consumers to submit an actual reservation request.

Reservation Form

The reservation form process is virtually identical to the availability form process. We will be referencing a physical document, loading it into our application, and appending it to our response document.

processRequest Logic

The processRequest function is responsible for loading the physical file containing the reservation form and adding it to our response document (see Listing 7-42.) The only difference is that we are adding a precedence check to see if this is a new session. There is no reason for this step to be the first in a session, because an availability request must be made before a reservation request will even be accepted. We are just catching this error early so that users won't go through the trouble of completing the form only to have it be rejected due to the business rules.

Listing 7-42: Reservation form responder logic

start example
 Case 4      '—Check if new session - if so, this request is illegal      If blnNewSession = True Then GoTo Precedence_Error      '—Load the reservation form      objDomWorking.Load ("http://www.architectingwebservices.com/        interfaces/hrsws/reservationFormResponse.xml")      '—Load the response into the Response Node      Set objNodeWorkingList = objDomWorking.selectNodes        ("hrs:reservationFormInteraction")      Set objResponseNode = objNodeWorkingList.Item(0)      Set objNodeWorkingList = Nothing 
end example

Sample Response

If we look at the response, we'll see that it is very similar to the availability form response. That is only logical, since we have the same header data and are using the same methodology in defining the data elements. (See Listing 7-43.)

Listing 7-43: Reservation form response

start example
 <?xml version="1.0"?> <hrs:reservationWS xmlns:hrs=" urn:reservationWS">       <hrs:serviceVariables>             <sessionID>1072</sessionID>             <stage>4</stage>       </hrs:serviceVariables>       <hrs:payload>             <hrs:reservationFormInteraction xmlns:hrs=               "urn:reservationFormInteraction">                  <reservationForm>                       <firstName>                            <name>firstName</name>                            <caption>First Name</caption>                            <maxLength>20</maxLength>                       </firstName>                       <lastName>                            <name>lastName</name>                            <caption>Last Name</caption>                            <maxLength>30</maxLength>                       </lastName>                       <homePhone>                            <name>homePhone</name>                            <caption>Home Phone Number</caption>                            <maxLength>15</maxLength>                       </homePhone>                       <homeAddress>                            <address>                                  <name>address</name>                                  <caption>Address</caption>                                  <maxLength>30</maxLength>                            </address>                            <address>                                  <name>address</name>                                  <caption>Address</caption>                                  <maxLength>30</maxLength>                            </address>                            <city>                                 <name>city</name>                                 <caption>City</caption>                                 <maxLength>30</maxLength>                            </city>                            <state>                                 <name>state</name>                                 <caption>State</caption>                                 <maxLength>2</maxLength>                                 <minLength>2</minLength>                            </state>                            <zipCode>                                 <name>zipCode</name>                                 <caption>Zip-Code</caption>                                 <maxLength>5</maxLength>                                 <minLength>5</minLength>                            </zipCode>                       </homeAddress>                       <couponCode>                            <name>couponCode</name>                            <caption>Coupon Code</caption>                            <maxLength>10</maxLength>                       </couponCode>                       <cardNumber>                            <name>cardNumber</name>                            <caption>Credit Card Number</caption>                            <maxLength>16</maxLength>                       </cardNumber>                       <nameOfOwner>                            <name>nameOfOwner</name>                            <caption>Name on Credit Card</caption>                            <maxLength>50</maxLength>                       </nameOfOwner>                       <cardIssuer maxValues="1">                            <name>cardIssuer</name>                            <caption>Card Issuer</caption>                            <option value=" Visa" caption=" Visa"/>                            <option value=" Mastercard" caption=" Mastercard"/>                            <option value=" American Express" caption=" American                              Express"/>                            <option value=" Discover" caption=" Discover"/>                       </cardIssuer>                       <ccExpirDate>                            <name>ccExpirDate</name>                            <caption>Credit Card Expiration Date</caption>                             <ccExpirMonth maxValues="1">                                   <name>ccExpirMonth</name>                                   <option value="1" caption=" Jan"/>                                   ...                                   <option value="12" caption=" Dec"/>                             </ccExpirMonth>                             <ccExpirYear maxValues="1">                                   <name>ccExpirYear</name>                                   <option value="2001" caption="2001"/>                                   <option value="2002" caption="2002"/>                                   <option value="2003" caption="2003"/>                                   <option value="2004" caption="2004"/>                                   <option value="2005" caption="2005"/>                                   <option value="2006" caption="2006"/>                             </ccExpirYear>                       </ccExpirDate>                       <billingAddress>                             <address>                                   <name>address</name>                                   <caption>Address</caption>                                   <maxLength>30</maxLength>                             </address>                             <address>                                   <name>address</name>                                   <caption>Address</caption>                                   <maxLength>30</maxLength>                             </address>                             <city>                                   <name>city</name>                                   <caption>City</caption>                                   <maxLength>30</maxLength>                             </city>                             <state>                                   <name>state</name>                                   <caption>State</caption>                                   <maxLength>2</maxLength>                                   <minLength>2</minLength>                             </state>                             <zipCode>                                   <name>zipCode</name>                                   <caption>Zip-Code</caption>                                   <maxLength>5</maxLength>                                   <minLength>5</minLength>                             </zipCode>                       </billingAddress>                  </reservationForm>             </hrs:reservationFormInteraction>       </hrs:payload> </hrs:reservationWS> 
end example

Reservation Request

We have arrived at the final step of this Web service workflow. Everything has been building up to this point, when a reservation request is submitted to the system for a person's travel plans. This step is a little different from all the others because we have to retrieve some data from the database, utilize a little business logic, and interact with the HRS system. The anticipated result will be a confirmation number that should assure the user that the reservation has been made.

Data Elements and Logic

We need to potentially retrieve three groups of data from our data store. The first one is the availability criteria from the last availability request the user submitted. This data will be used to limit the amount of data requested of the user, as well as sent between the service and consumer, in the reservation request process.

To expose the criteria to the application, we will build a simple stored procedure called getAvailabilityCriteria. It accepts a session ID as its lone parameter, and a recordset is returned with the criteria that were saved through the setAvailabilityCriteria stored procedure, as shown in Listing 7-44.

Listing 7-44: The getAvailabilityCriteria stored procedure

start example
 CREATE PROCEDURE dbo.getAvailabilityCriteria(@sessionID int) AS Select check_in_date,check_out_date,number_adults,number_children,coupon,   smoking from session_avail_checks where hrssession = @sessionID GO 
end example

The second piece of data we need is for transcribing room type descriptions to room type codes. As we identified in our design process, the HRS availability interface returns the room type as a string, but the reservation interface requires a code. To handle this appropriately, we need our own index matching the room codes with the room names. There are only four room types available today, but by putting this in the database, we will be able to easily adapt to any changes in the HRS system.

The table will consist of two simple columns, one for the code and one for the name, as seen in Table 7-8.

Table 7-8: HRS_roomTypes Implementation

COLUMN

DATA TYPE

SIZE

NOTES

RoomType_code

int

4

Primary key

RoomType_name

varchar

20

 

The table will initially be populated with four records, show in Table 7-9.

Table 7-9: Initial Data Values in Content_Ref_Table

ROOMTYPE_CODE

ROOMTYPE_NAME

101

Suite

201

Value

301

Business

401

Premiere

Now all we need is the stored procedure to retrieve this information. For this situation, all we need to do is retrieve a code for the name that we have. However, it is easy to see the possibility for needing to do the opposite sometime later: retrieve a name given a code. For this reason we will make the stored procedure slightly more robust by allowing both values as inputs and outputs. Then we just need the If...Then logic to determine which process to run. The key for making this determination is the roomType_code input. If the value is 0, then look up the code for the name, otherwise look up the name for the code as seen in Listing 7-45.

Listing 7-45: The getRoomTypes stored procedure

start example
 CREATE PROCEDURE dbo.getRoomTypes(@roomType_code int output,   @roomType_name varchar(20) output) AS if @roomType_code = 0      Select @roomType_code = roomType_code from HRS_roomTypes      where roomType_name = @roomType_name else      Select @roomType_name = roomType_name from HRS_roomTypes      where roomType_code = @roomType_code GO 
end example

The last data reference is related to reservation failures, so it won't be necessary for every request. These are not system failure errors, but processing errors due to the sudden lack of availability, credit card problems, etc. If there is a problem with the reservation process, a code is returned from the HRS system. We know what the codes stand for, but no description is provided by the HRS interface. To address this issue, we will maintain our own table with codes and a human-readable description available to consumers and/or users.

The table itself is called HRS_errors and has a simple layout, shown in Table 7-10.

Table 7-10: HRS_errors Implementation

COLUMN

DATA TYPE

SIZE

NOTES

Error_key

tinyint

1

Primary key

Error_descrip

varchar

255

 

Any confirmation returned with a value below 100 is an error code. Through this table we can provide the descriptions we want for each code. The error codes that will initially populate this table are listed in Table 7-11.

Table 7-11: Initial Data Values in Content_Ref_Table

ERROR_KEY

ERROR_DESCRIP

0

"The reservation you requested is unavailable at this time."

1

"The personal information supplied is incomplete. Please check the information and try again."

2

"The credit card provided for this reservation could not be approved at this time. Please try another credit card."

3

"The credit card information is not valid. Please check the information and try again."

To access this error data we have a simple stored procedure that accepts the error code and returns the corresponding description, as shown here:

 CREATE PROCEDURE dbo.lookupError(@error int,@errorText varchar(255) output)   AS select @errorText = error_descrip from HRS_errors where error_key = @error GO 

Now that we have our data elements and logic defined, let's look at implementing all the functionality for our reservation requests in the processRequest function.

processRequest Logic

Unlike the availability request and hotel detail process, we will not be creating a separate function to contain logic for this request. A lot of data is involved in this request, and there probably wouldn't be much benefit to moving it around between functions.

The first step in processing this request is checking for precedence. This is done with a check of the blnNewSession value. If it is set to true, we know that an availability request has not been made. However, this only checks the existence of the session. This does not qualify that an availability request was actually made. We will validate that later when we retrieve the availability criteria.

Prior to that we have a lot of parsing to do. We will take each value in the reservation request document and save it as a local variable. This task is easier than it was in some of our other processes because there are no arrays and very few optional fields. Parsing XML is relatively simple when you specify which nodes you are accessing and can trust that they will be there.

With that, let's go ahead and take a look at our responder code, shown in Listing 7-46, which will provide the response to the reservation request.

Listing 7-46: Reservation interaction responder code

start example
 Case 5      '—Check if new session - if so, this request is illegal      If blnNewSession = True Then GoTo Precedence_Error      '—parse xml data for HRS reservation call      Dim objMakeReservation As reservationSystem.clsHRS      Dim intConfirmNum As Long      Set objNodeWorkingList = objDomRequest.selectNodes("hrs:reservationWS/        hrs:payload/hrs:reservationInteraction/hrs:reservationRequest")      Set objNodeWorking = objNodeWorkingList.Item(0)      '—Extract hotel data      strHotelID = objNodeWorking.selectSingleNode        ("hrs:hotelData/hotelID").nodeTypedValue      strRoomType = objNodeWorking.selectSingleNode        ("hrs:hotelData/roomType").nodeTypedValue      '—Extract personal data      strFirstName = objNodeWorking.selectSingleNode        ("hrs:personalData/firstName").nodeTypedValue      strLastName = objNodeWorking.selectSingleNode        ("hrs:personalData/lastName").nodeTypedValue      strHomePhone = objNodeWorking.selectSingleNode        ("hrs:personalData/homePhone").nodeTypedValue      strHomeAddress1 = objNodeWorking.selectSingleNode        ("hrs:personalData/homeAddress/address").nodeTypedValue      If objNodeWorking.selectSingleNode("hrs:personalData/homeAddress/address")        .nextSibling.nodeName = "address" Then      strHomeAddress2 = bjNodeWorking.selectSingleNode("hrs:personalData/        homeAddress/address").nextSibling.nodeTypedValue      strHomeCity = objNodeWorking.selectSingleNode        ("hrs:personalData/homeAddress/city").nodeTypedValue      strHomeState = objNodeWorking.selectSingleNode        ("hrs:personalData/homeAddress/state").nodeTypedValue      strHomeZip = objNodeWorking.selectSingleNode        ("hrs:personalData/homeAddress/zipCode").nodeTypedValue      strBillAddress1 = objNodeWorking.selectSingleNode        ("hrs:personalData/billAddress/address").nodeTypedValue        If objNodeWorking.selectSingleNode("hrs:personalData/billAddress/address")        .nextSibling.nodeName = "address" Then _      strBillAddress2 = objNodeWorking.selectSingleNode("hrs:personalData/        billAddress/address").nextSibling.nodeTypedValue      strBillCity = objNodeWorking.selectSingleNode("hrs:personalData/        billAddress/city").nodeTypedValue      strBillState = objNodeWorking.selectSingleNode("hrs:personalData/        billAddress/state").nodeTypedValue      strBillZip = objNodeWorking.selectSingleNode("hrs:personalData/        billAddress/zipCode").nodeTypedValue      strCCnumber = objNodeWorking.selectSingleNode("hrs:personalData/        hrs:ccData/number").nodeTypedValue      strccexpdate = objNodeWorking.selectSingleNode("hrs:personalData/        hrs:ccData/expirationDate").nodeTypedValue      strCCName = objNodeWorking.selectSingleNode("hrs:personalData/        hrs:ccData/nameOfOwner").nodeTypedValue      strccissuer = objNodeWorking.selectSingleNode("hrs:personalData/        hrs:ccData/issuer").nodeTypedValue      '—Lookup previous session data in database for criteria information      Set objCommand = New Command      Set objRS = New Recordset      With objCommand           .ActiveConnection = objConn           .CommandText = "getAvailabilityCriteria"           .CommandType = adCmdStoredProc           .Parameters.Append .CreateParameter("sessionID", adInteger,             adParamInput, , CInt(strSession))           Set objRS = .Execute      End With      If Not objRS.EOF Then           '—Set Criteria values           strCheckIn = objRS(0)           strCheckOut = objRS(1)           strAdults = objRS(2)           strChild = objRS(3)           strSmoke = objRS(5)           If objRS(4) <> Null Then strCoupon = objRS(4)      Else           GoTo Precedence_Error      End If      '—Convert room type to integer      Set objCommand = New Command      With objCommand           .ActiveConnection = objConn           .CommandText = "getRoomTypes"           .CommandType = adCmdStoredProc           .Parameters.Append .CreateParameter("intRoomType", adInteger,            adParamInputOutput, , 0)           .Parameters.Append .CreateParameter("strRoomType", adChar,            adParamInputOutput, 20, strRoomType)           .Execute           strRoomType = .Parameters("intRoomType").Value      End With      '—Call HRS interface      Set objMakeReservation = New clsHRS      intConfirmNum = objMakeReservation.makeReservation(CLng(strHotelID), _        CInt(strRoomType), _        CDate(strCheckIn), _        CDate(strCheckOut), _        CInt(strAdults), _        CInt(strChild), _        strFirstName, _        strLastName, _        strHomeAddress1, _        strHomeAddress2, _        strHomeCity, _        strHomeState, _        strHomeZip, _        strHomePhone, _        strCCnumber, _        CInt(strHotelID), _        CInt(strHotelID), _        strccissuer, _        strBillAddress1, _        strBillAddress2, _        strBillCity, _        strBillState, _        strBillZip, _        strCoupon, _        CBool(strSmoke))      strPayload = "<confirmationID>" & CStr(intConfirmNum) & "</confirmationID>"      strPayload = strPayload & "<phoneNumber>800-555-1234</phoneNumber>"      strPayload = "<hrs:reservationResponse>" & strPayload &        "</hrs:reservationResponse>"      strPayload = "<hrs:reservationInteraction xmlns:hrs=        "" urn:reservationInteraction"">" & strPayload &        "</hrs:reservationInteraction>"      '—Load the availability results into a DOM      blnLoad = objDomWorking.loadXML(strPayload)      If Not blnLoad Then GoTo Data_Error      '—Check if confirmation is an error      If intConfirmNum < 100 Then            '—look up error codes            Set objCommand = New Command            With objCommand                 .ActiveConnection = objConn                 .CommandText = "HRSerrorLookUp"                 .CommandType = adCmdStoredProc                 .Parameters.Append .CreateParameter("error", adInteger, adParamInput                    ,, intConfirmNum)                 .Parameters.Append .CreateParameter("errorText", adChar,                    adParamInput, 255, strCoupon)                 .Execute            End With            'append description to payload            Set objNodeWorking = objDomWorking.createElement("description")            objResponseNode.nodeTypedValue =              objCommand.Parameters("errorText").Value            objDomWorking.selectSingleNode("hrs:reservationInteraction").appendChild              objNodeWorkingList      End If      Set objNodeWorkingList = objDomRequest.selectNodes("hrs:reservationWS/        hrs:payload/hrs:reservationInteraction/hrs:reservationRequest")      objDomWorking.selectSingleNode("hrs:reservationInteraction").appendChild        objNodeWorkingList.Item(0)      '—Load the response into the Response Node      Set objNodeWorkingList =        objDomWorking.selectNodes("hrs:reservationInteraction")      Set objResponseNode = objNodeWorkingList.Item(0)      Set objNodeWorkingList = Nothing 
end example

Sample Response

In a sample response, we will see just how small a portion of the entire document is made up of the resulting information. Our confirmation ID is slipped in between our service variables and the copy of the reservation request (see Listing 7-47), which we decided to include in our design process.

Listing 7-47: Response from reservation request

start example
 <?xml version="1.0"?> <hrs:reservationWS xmlns:hrs=" urn:reservationWS">       <hrs:serviceVariables>             <sessionID>1086</sessionID>             <stage>5</stage>       </hrs:serviceVariables>       <hrs:payload>             <hrs:reservationInteraction xmlns:hrs=" urn:reservationInteraction">             <hrs:reservationResponse>                   <confirmationID>678</confirmationID>                   <phoneNumber>800-555-1234</phoneNumber>             </hrs:reservationResponse>                   <hrs:reservationRequest xmlns:hrs=" urn:reservationRequest">                         <hrs:hotelData>                               <hotelID>1234</hotelID>                               <roomType>Suite</roomType>                         </hrs:hotelData>                         <hrs:personalData>                               <homeAddress>                                     <address>301 Main St.</address>                                     <city>Edwardsville</city>                                     <state>IL</state>                                     <zipCode>62025</zipCode>                               </homeAddress>                               <firstName>Jamie</firstName>                               <lastName>Langenbrunner</lastName>                               <homePhone>618-555-1111</homePhone>                               <billAddress>                                     <address>301 Main St.</address>                                     <city>Edwardsville</city>                                     <state>IL</state>                                     <zipCode>62025</zipCode>                               </billAddress>                               <hrs:ccData>                                     <number>4444333322221111</number>                                     <issuer>Visa</issuer>                                     <expirationDate>2003-05-01</expirationDate>                                     <nameOfOwner>James Langenbrunner</nameOfOwner>                               </hrs:ccData>                         </hrs:personalData>                   </hrs:reservationRequest>             </hrs:reservationInteraction>       </hrs:payload> </hrs:reservationWS> 
end example

The only difference between this response and a failed reservation request response is that the confirmation ID would be a number lower than 100 and it would be followed by a description field containing the text from our HRS_errors reference table.

This actually completes all of the request-specific logic necessary for our Web service. This does not mean that we are done, however. We still have two more steps to implement to complete the entire service: the security model and the presentation model for the isolated service.

Security Model

Now that we have built and tested our Web service, it would be appropriate to go back and modify the program to secure it. Some might argue that this should have been set up with the initial base service, but I did not simplify the troubleshooting process. We had already developed our security model, so we wouldn't code ourselves into a corner, but adding it at such an early stage is taking on more effort than necessary.

Since we are using client certificates, you might be wondering why we need to validate the consumer on a data level. Although this method could be used to authenticate consumers, the purpose here is less about authentication and more about session management.

Because the users do not connect directly to our service, we have no reliable means for tracking session activity between the requests of various users via various consumers. We have the service variables, but that is simply XML data and is relatively easy to spoof. We chose not to maintain consumer IDs in the service variables for this very reason. Being able to reference a consumer through the consumer's client certificate gives us a means for "grounding" our sessions. This limits the potential for consumers to spoof the sessions of other consumers. Of course they can still spoof the sessions in their domain, but they control their users' experience regardless. They can abuse their own users regardless of our service, so we have to accept that possibility.

The security for this service is similar to the security we set up for the Web service call of our COM object in Chapter 6. In that example, we set up our service to only accept calls coming from trusted consumers. We will simply extend that functionality to integrate the consumer identity into our Web service workflow. This integration will involve two additional steps: extracting the certificate data and validating the consumer.

Note 

If you have questions about setting up the site security or client certificates for our Web service, review the section in Chapter 6.

Extracting the Certificate Data

The first thing we have to do is modify our listener to extract the information provided by the client certificate. This data is actually exposed through the ASP request object in the form of a dictionary object. To reference this data, you simply need to create the dictionary object and set it to the request.clientcertificate method, as shown here:

 set objDic = Server.CreateObject("Scripting.Dictionary") set objDic = request.clientcertificate() 

As an experiment, you could walk through the entire dictionary to see all of the data provided by the client certificate. You might be surprised by the amount of data you see by using the following code:

 For Each key in Request.ClientCertificate      Response.Write(key & "= "& Request.ClientCertificate(key) & "<BR>") Next 

Notice by all the key names that this information is structured in an active directory format. If you are familiar with active directories or LDAP systems, you will likely recognize the naming convention of the data values.

We want to reference one specific piece of information from this dictionary object, the subjecto. This name equates to the organization the client carrying this certificate belongs to. We will want to utilize this field over the others because they can be user dependent. We are treating the organization as our consumer, not an individual user. It would be just as easy to utilize any other data element in the certificate, so feel free to reference whatever you need for your own implementations.

Listing 7-48 shows what our listener now looks like with the changes to extract the client certificate data.

Listing 7-48: Web services listener with integrated security

start example
 <%@ Language=VBScript %> <%      dim objHRSRequest      dim objDOM      set objHRSRequest = Server.CreateObject("HRSWS.clsListener")      set objDic = Server.CreateObject("Scripting.Dictionary")      set objDOM = server.createobject("MSXML2.DOMDocument")      objdom.async = false      objdom.load(request)      set objdic = request.clientcertificate()      Response.ContentType = "text/xml"      response.write objHRSRequest.processRequest(1,objdom.xml,objdic("subjecto"))      set objdom = nothing      set objdic = nothing      set objhrsrequest = nothing %> 
end example

Validating the Consumer

Now that we have programmatically captured the consumer's identity, we need to validate it against the consumers in our data model. The specific field we are comparing against is consumer_cert. This name was purposely left generic so that its purpose was clear and could serve as a reference to any certificate data element. After all, we may decide to switch to a user-based system instead of an organization-based one.

Since we already have the consumer data in our database, the first thing we need to add is a stored procedure for accessing the data. We will pass it the consumer name and get back a consumer ID and a Boolean representing a successful or unsuccessful match. This stored procedure, called getConsumerID, is shown in Listing 7-49.

Listing 7-49: The getConsumerID stored procedure

start example
 CREATE PROCEDURE dbo.getConsumerID(@consumerName varchar(30),   @consumerID int output, @found bit output) AS select @consumerID = consumerID from consumers where consumer_cert =   @consumerName if @consumerID is NULL      select @found = 0 else      select @found = 1 GO 
end example

What I have not addressed here is how to handle the consumers that are not present in this data store. If the consumer has gotten to the point of making this request, it obviously has a client certificate that is accepted by our site. If the assumption is that anyone with a valid client certificate should be in the table, we could easily add the code to automatically add such consumers. However, this is a bit too optimistic an approach for this senario.

I could foresee a situation in which a partnership with a consumer has ended and the consumer's client certificate is still valid. This automation would automatically add the consumer back into the database. Furthermore, if you were to ever expand your Web service offerings to include other services, not all valid clients may be allowed access to all services. This gets back into the Web service site security topics discussed in Chapter 6.

For this implementation, we will take the pessimistic approach and leave the consumer accounts as is. What we might want to do is set up some email notification in the case of a failed consumer certificate match. That would at least notify us as soon as such a situation occurs and allow us to anticipate any calls from consumers! This could be done through the stored procedure and the email services of SQL Server 2000, but I will not lead you down that tangent effort here.

Authentication

Now we need to add the authentication check to our processRequest function. We will use the same connection and command objects we are using for other database calls to call the getConsumerID stored procedure. Based on the setting of the sessionFound parameter, we will either set the consumer ID or produce an error through the consumer_error procedure as seen in Listing 7-50.

Listing 7-50: Responder code for authentication check and error handling

start example
 Set objCommand = New Command With objCommand       .ActiveConnection = objConn       .CommandText = "getConsumerID"       .CommandType = adCmdStoredProc       .Parameters.Append .CreateParameter("consumerName", adChar, adParamInput,         30, consumerName)       .Parameters.Append .CreateParameter("consumerID", adInteger, adParamOutput)       .Parameters.Append .CreateParameter("sessionFound", adBoolean,         adParamOutput)       .Execute       If .Parameters("sessionFound") Then           intConsumerID = .Parameters("consumerID").Value       Else           GoTo Consumer_Error      End If End With ... 'near the end of the function... Consumer_Error:      processRequest="<error><level>critical</level><description>The Web service      could not validate the consumer making the request. Please contact the      provider to establish an account for this service.</description></error>"      GoTo End_Proc 
end example

This completes our security model implementation. As you can see, it was fairly easy to add after our implementation was complete and did not encumber us while troubleshooting the functionality. There may be times this approach cannot be taken, but when available, I think you will benefit from it through better productivity.

Presentation Model

The only remaining component of our implementation is the presentation model. In fact, this only applies to our isolated service model, so for all intents and purposes, our embedded service model is already finished!

For our isolated service consumers, we will provide XSL templates that they can use to transform our data-oriented responses into presentable HTML interfaces. To implement this model, there are two things we need to do: develop the XSL documents and add the XSL references to our responses.

Developing the XSL Documents

Our XSL templates will effectively be utilizing XHTML-compliant data (as discussed in Chapter 4). Since this is a key component of a consumer implementation of a Web service, I will not go into the technical details of making the templates. In this chapter, I will instead focus on the approach providers should take whenever they decide to provide templates for their consumers. For information on building XSL templates for Web services, please read on to Chapter 8, where we will build a consumer application for this service. You can also go to http://www.architectingwebservices.com to download and view the templates that are provided for this Web service.

The purpose of an XSL stylesheet exposed by a provider is to act as a template for a suitable interface containing the necessary data from the response. There are two intentions consumers will have when utilizing your templates, and you should keep them in mind.

The first is that the consumer will want to get up and running very quickly. It should work right away "as is" with your responses to provide all the necessary functionality to be acceptable for your Web service. The most effective way to meet this expectation is to define a minimal interface that meets the basic requirements. The focus will not be making it look fancy or "finished," but providing a base that can accommodate a given look and feel. You aren't going to guess how your consumers will want to present the interface, so instead of trying to meet everyone's needs, try to keep it simple and make it flexible.

The key to making the stylesheet flexible is having clearly defined variables that provide easy access to things like colors and aesthetic attributes. For example, the top of the stylesheet could include a section like the following:

 <xsl:variable name="backgroundColor" select="white"/> <xsl:variable name="fontColor" select="blue"/> <xsl:variable name="frameBorder" select="true"/> 

You can then reference these variables wherever appropriate in your XSL document to apply these attributes. Again, we will look at implementing XSL in much more detail in Chapter 8.

The second intention consumers will have is to learn how your template works so that they can learn enough about XSLT to start modifying the template, if not make their own. This is very desirable because it allows your consumers to be more knowledgeable and self-reliant. A knowledgeable consumer is more likely to provide you good feedback on your Web service so that you can make changes and enhancements to it.

To aid your consumers with this, it is important to provide simple, well-written XSL documents. Overly complicated approaches to building the interface will only intimidate or confuse your consumers, which will set back their attempts to learn the technology.

The other thing you should do is provide very good comments in your template. This is a little bit of a double-edged sword, because the more comments you have, the bigger a drain on resources there will be to transfer the template and work with it. Keep in mind that I said "very good comments," not "a lot of comments." Try to be succinct, but also try to keep any readers informed as to what you are doing at different points in the template.

Once the stylesheets for our hotel reservation Web service have been created, the only thing left to do is add a reference to them in our response.

Adding the XSL Reference

In our design process, we decided to add the stylesheet references for our isolated service consumers in the service variables of our response. The only other decision that needs to be made is how to name the templates.

To keep it simple, we will name the stylesheets according to the interaction responses for which they are built. For instance, the stylesheet for the availability form response will be availabilityFormInteraction.xsl. This will allow us to build the reference dynamically off the response, as opposed to keeping another data source for mapping to an independent naming convention. The code for building the node and adding it to the response is shown in Listing 7-51. This code will go in the else case of our serviceType check as was commented in our base service.

Listing 7-51: XSL template node creation and response modification

start example
 Set objNodeWorking = objDomResponse.createElement("stylesheet") '—Assign path and filename to template objNodeWorking.nodeTypedValue =   "http://www.architectingwebservices.com/hrsws/stylesheet/" &   Right(objDomResponse.selectSingleNode("hrs:reservationWS/hrs:payload")   .firstChild.nodeName, Len(objDomResponse.selectSingleNode   ("hrs:reservationWS/hrs:payload").firstChild.nodeName) - 4) & ".xsl" '—Add the node to the response DOM objDomResponse.selectSingleNode("hrs:reservationWS/hrs:serviceVariables")   .appendChild objNodeWorking 
end example




Architecting Web Services
Architecting Web Services
ISBN: 1893115585
EAN: 2147483647
Year: 2001
Pages: 77

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net