Database Publishing on the Internet with Visual FoxPro

By Rick Strahl

http://www.west-wind.com/

Revised session notes from my 1996 Visual FoxPro DevCon session.
Updated 04/14/1997

Download this document (HTML)
Download Word '97 Document
Download Powerpoint '97 slides


Overview

This white paper will walk you through some of the tools available to build Web based applications with Visual FoxPro. Focus will be on building server side applications by introducing some of the tools that can take advantage of Visual FoxPro data and code to build high performance backends.

This paper will cover the following areas:

Introduction

Building applications that are integrated with Internet technology and can connect databases to Web sites will likely become an important aspect in your software development in the future, if it hasn't become so already. We're just at the beginning of the move toward 'Active' Web content and while we'll surely see improvements in the tools available down the line, applications built with Visual FoxPro can provide extraordinary power, versatility and speed using standard PC hardware to drive high powered Web sites today.

Internet Development is exploding

People are flocking to the Internet and the World Wide Web in particular by the millions. Because of the both the hype and the actual traffic on the 'Net, the Internet is an exploding market as businesses are trying to integrate the Internet into their existing business strategies.

There is tremendous demand for developers who can build the dynamic content necessary to build truly useful, distributed business applications that can run over the open Internet or the local Intranet.

Active, Database Applications are in high demand

Database applications are the key to building active Internet applications. Until recently static content via plain HTML pages has been the standard fare on the World Wide Web. However, for conveying lots of information the static HTML concept falls apart quickly and becomes a maintenance nightmare.

The true potential of the Web lies in giving users access to data that is always up to date, allows them to see only what they chose to look at by allowing the data to be filtered and queried to display only small, appropriately sized chunks at a time.

Visual FoxPro is ready for the Internet today

Visual FoxPro is more than ready for this challenge. I've been involved in building several high volume Web sites that use Visual FoxPro as the database backend. Not only did Visual FoxPro perform extremely well under heavy load, but it also allowed creation of applications in record time and with a budget that was a fraction of the nearest competitor’s bid.

Visual FoxPro provides exceptional database access speed that's unrivaled by ODBC based tools and even full blown SQL servers in many circumstances. In addition you can leverage your existing FoxPro skills and code base to build quality applications with full featured logic using the data centric and object oriented, FoxPro language. If designed properly, applications can also be scaled to handle just about any request load scaling to multiple processors or even multiple servers on separate machines across a network.

Why build Web Applications?

If you’ve read through the computer trade papers or even the mainstream computer press you’ve probably noticed that the Web is affecting just about every aspect of computing these days. Software developers and tool vendors alike are focusing on the Web as the next development platform. Clients are asking about Net technology and how they can take advantage of the ‘distributed’ envrionment which the Web offers.

For better or worse, the Web is here to stay. There are a number of very important steps forward, but at the same time a few blems that take us a step back in the area of application development. In this section and the following one I’d like to point out some of the strengths and weaknesses of Web based application development.

Distribute widely, administer centrally

The Internet could become the ultimate client/server platform. It provides:

Universal Client Interface

The Web Browser is quickly becoming one of the killer applications that come around and change the computing environment. The quick acceptance of this interface is driving changes in software design that is moving more and more technology towards a Web based interface.

Application Platform of the future

Web browsers have brought about drastic changes in the software development field. One look at the latest software offerings from all the major software houses shows this in evidence: Internet connectivity and output options for creating HTML are evident wherever you look. This integration will only become more prelevant with Microsoft and Netscape's plans to integrate the desktop environment more completely with the Browser in forthcoming versions of Windows.

Development tools of all types now have at least rudimentary tools to connect to the Internet. Microsoft has been previewing the next release of Internet Explorer which promises to integrate the Web browser directly into the operating system, where the desktop and integral file operations as well as access the network occur within a familiar browser interface. It's all geared towards integrating the Internet more transparently into the operating system. The hyperlinked nature of HTML (and the supporting ActiveX and Java technologies) makes it possible to transparently tie together the local machine, the interoffice workgroup or Intranet applications and the entire Internet. Welcome to the Network machine!

Limitations of Web Applications

There are many benefits to building an application that runs over the Web. But it's also extremely important to understand the limitations that you will face when building Web applications. They are not insurmountable, but they do require rethinking application development to some extent.

Configuration Issues

Web Development is definitely more involved than building a standalone application using a visual tools such as Visual FoxPro or Visual Basic. For one thing you are dealing with a larger number of entities rather than just a single environment: The Web server, a connector application of some sort and the backend application, HTML pages and code. The complexity of how many different pieces are involved varies between the various approaches to development. Typically, you’ll use a Browser to ‘test’ your code, rather than simply running an application.

You need to have a basic understanding of how the components fit together in order to make all the pieces work together. The promise of component based software is starting to materialize with the Web, but as of now, it’s not necessarily easy to make the components play nice.

Interface limitations of HTML

If you believe the trade press, the Web is the nirvana of application development that will solve all your problems – yeah, right! The tools that are available today are downright primitive when compared to full visual development tools and Web applications reflect this in rather simplistic interfaces that are used to present forms and interaction with the user.

Although typical HTML output can be very visual, there are various limitations in HTML that require re-thinking your typical database application user interface.

Mostly non-visual Development

For the most part Web based application development is non-visual. While you can use visual HTML editors like FrontPage, Visual InterDev or WebEdit to build the HTML pages you display on browsers, the actual application code you write is usually transaction based involving mostly straight database code (queries, validations, inserts etc) and either generating the HTML via code or loading HTML pages from disk to evaluate embedded logic.

Server based programming

Web application request handlers are basically server scripts, which are non-interactive and transaction based. Each link, or form request generates a hit on the server which in turn runs the request handler in response. Each request needs to be fully self contained and establish its own environment as HTTP is a stateless request that does not provide for maintaining your state. For example, a simple operation such as going to the next record requires that you pass a request to the server with a record ID of the current record so the request can figure out to go to the current record, then SKIP to the next record, create the full updated HTML page and send it back to the browser, where in a standalone application you'd do nothing more than a SKIP and THISFORM.Refresh().

Web programming usually involves:

How The Active Web works

Figure 1.1 shows how Microsoft's Active architecture binds the client and server sides together. When looking at the figure keep in mind the strict separation between the client and server sides. Although some of the components like ActiveX controls and ISAPI extensions are Microsoft specific, the frameworks by Netscape and others look surprisingly similar.

Figure 1.1 - Browser and Web Server relationship. Note the distinct line between the two.

The Browser provides the Active interface

The Web browser provides the interface to an application or page viewer. The browser provides the interactive, visual face of a Web application. The interface consists mostly of HTML text along with graphics and basic input forms that can be embedded inside of HTML documents. All data input is handled via HTML formatted pages that are interpreted and then displayed by the browser.

Browser scripting languages like VBScript and JavaScript can be used to tie some logic to the HTML page. Scripting provides conditional output creation and an object/event model that allows manipulation of various interface and browser objects as well as responding to events fired by various form controls and form events. Keep in mind though: There is no direct data access from the client side, so you can't do data validation based on a table lookups unless all the information is somehow created into the HTML page directly. If you need data to be displayed in the browser the data needs to be generated into the HTML page at the server.

ActiveX Controls and Java Applets can be used to enhance Web pages with high power add-ins or operating system specific components. The controls can be manipulated using VBScript using a typical Property, Event, Method mechanism.

ActiveX and Java

ActiveX Controls and Java Applets can be used to enhance Web pages with high power add-ins or operating system specific components. The controls can be manipulated using VBScript or JavaScript using a typical Property, Event, Method mechanism. While it's getting easy to build ActiveX and Java controls with tools such as Visual Basic 5, Delphi and Java applet designers it's not always easy to get any data captured in these controls to the server.While you can build a slick user interface control that captures user input, there's no direct way to send this information back to the server. Rather the properties of the control/applet need to be mapped to existing or hidden form variables so the server can receive them via the HTTP protocol used to submit the data.

Web Server provides data/application connectivity

On the other end of the connection sits the Web server. The Web server’s main responsibility is, well, to serve content. Traditionally this content has been static HTML pages served from disk, but the Web server’s role is expanding to provide the basic logic to interact with backend applications via the ISAPI interface.

The Web server is responsible for providing the database access and the connectivity to the actual processing application. The Web server itself knows nothing about applications and calls helper scripts (ISAPI extensions) to do the work for it.

ISAPI is the building block for server side extensions

Typically the server calls a ISAPI or Common Gateway Interface (CGI) extension script which is responsible for returning HTTP compliant output. The Internet Server API (ISAPI) is a highly efficient Windows based API that allows extensions to run in the Web server's address space, which make them very fast and resource friendly. ISAPI extensions can either be self-contained and create the required output on their own or act as a connector and call another application to perform the actual request processing for it.

Keep in mind that ISAPI and CGI both are interfaces only and do not comprise a specific language implementation. ISAPI can be implemented in any language that can create true Win32 DLLs. CGI can be implemented by any language that supports creation of EXE files and can read and write to and from Standard Input and Output.

What’s important to remember is that Visual FoxPro never runs as an ISAPI application directly – it lacks the ability to create an ISAPI extension directly! Rather an ISAPI extension interacts with Visual FoxPro via some sort of messaging mechanism (OLE Automation, DDE, file based messaging etc.). If implemented correctly this process can be extremely fast and efficient. Both FoxISAPI and Web Connection use this approach to let you use Visual FoxPro to build application logic.

Figure 1.2 shows how ISAPI is the building block of most of the Web server extensions that Microsoft is creating. All the major tools provided for IIS by MS including the MS Internet Database Connector, and the Active Server framework are implemented via the ISAPI interface. FoxISAPI and Web Connection which will be discussed later on are also implemented using ISAPI as the connector interface and qualify as custom connector applications on the chart.

Figure 1.2 - Microsoft's Internet Server API is the building block for Web server extensions

What you need to get started

Here's a short list of hardware and software required to run a Web application:

Fast Pentium box (133Mhz/32-64megs)

While you can get by with smaller machines I would recommend this as a good baseline installation. I've run several Web sites on hardware as low as a 486-66 with 32 megs running NT 3.51 with decent results, but if your site gets more than a few thousand backend hits day the above is a good minimum.

For high volume sites multiprocessor boxes are the preferred way to go. Dual processors allow the Web server and Visual FoxPro to not compete for CPU cycles as much. Also, additional memory (128megs or more) can provide dramatic improvements in database (especially Read operations) and Web server performance as NT uses the memory for disk caching.

Windows NT Server (recommended)

Windows NT Server is an excellent platform for running a Web server and for acting as an application server. Many Web servers will run on Windows 95 and NT Workstation, but Windows 95 Web services are noticeably slower and NT Workstation has some serious licensing limitations that make it unsuitable for public Web server use, although it provides the same performance as NT Server.

Included with NT Server 4.0:

Connector Interface/Application

You'll also need a 'connector' application that handles tying your FoxPro data the Web server. A connector application is a script tool that provides services for accessing another application or a scripting engine via an ISAPI DLL.

Active Server Pages uses an ISAPI extension to provide the interpreting of .ASP files in a specialty DLL. FoxISAPI and Web Connection both use an explicit connector DLL that is called directly of an HTML link.

Web browser

In order to test your application you need a Web browser. I'll be using Internet Explorer 3.0 here, but any late browser should do. Try and get one of the latest browsers from Microsoft or Netscape as they make up over 90% of the browser market and provide the most advanced features that you will encounter when cruising the Web.

Basic HTML skills

You knew this was coming, right? Yes, in order to build Web applications you need to have at least a passing acquaintance with HTML. While it's possible to build HTML graphically using HTML editors like FrontPage, PageMill, NetObject etc, it's often required to generate HTML that gets inserted into existing documents - in order to do that you have to know a little about the various tags associated with text formatting. It's easy and best picked up by looking at the source of existing HTML pages.

Everything can run on one box!

For development purposes you can set up a single machine to serve as your Web server and development platform. You don't need to be on network as you can access the Web server via its local IP address (127.0.0.1 or localhost).

Active Server Pages

Active Server Pages (ASP) is part of Microsoft Internet Information Server 3.0 and can be downloaded from the Microsoft Web site (http://www.microsoft.com/iis). In short, Active Server Pages is a sophisticated, server side, object based scripting engine that allows mixing code and HTML in the page using (currently) Visual Basic Script or JavaScript.

Figure 1.3 – Active Server is composed of a number of objects that are managed by the ASP.DLL ISAPI extension.

Tight integration with IIS
ASP is actually a part of Internet Information server and installs when you upgrade IIS 2.0 to 3.0. The integration into the server itself is very smooth by implementing an ISAPI extension with a script map (a mechanism that maps the .ASP extension to the ASP.DLL) to provide transparent support for interpreting any page with .ASP extension. This mechanism provides the illusion to the developer of simply editing an HTML page and enhancing it with dynamic script code.

Keep in mind the difference between client and server side scripting. Active Server is server side scripting which means the .ASP is evaluated by the server before it is sent back to the browser. This means that even though you're using VBScript any browser can see the resulting evaluated HTML that gets sent back to the browser (assuming you generated compliant HTML). Client side scripting occurs on the Browser and is browser specific. You can mix Client and Server side scripting by using some special tags in the HTML <SCRIPT> tag.

Great care has been taken to provide an efficient environment for Active Server. The ISAPI extension handles multi-threading of requests and connection pooling for ODBC connections to provide fast access to dynamic content.

Object Based Architecture

The entire Active Server framework is based on components that interact to provide access to the entire environment to the developer. Figure 1.3 shows the base components that ship with Active Server. The various components provide access to the vital pieces that are required to build sophisticated Web applications.

Here’s a list of the base framework components and what they do:

<%
'*** Open the Connection - create a new object or reuse existing
IF IsObject(Session("goConn")) THEN
      SET Conn=Session("goConn")
ELSE
      Set Conn = Server.CreateObject("ADODB.Connection")
      Set Session("goConn")=Conn
END IF
%>

Database Connectivity with Active Data Object (ADO)

Active Server provides built-in database connectivity via Active Data Objects or ADO. This ODBC based server component (yes, it’s a COM based engine much like VB’s DAO or RDO). It provides the following features:

Here’s an example of using ADO for logging custom log information logged into a Visual FoxPro database with one link and then retrieving the log with another:

'*** ODBCLOG.ASP
'*** Logging page - this page logs visitor info into
'    a VFP table via ADO
<%
'*** If visitor doesn't have a cookie log the hit as a new visitor
if Request.Cookies("wwVisitor") = "" then
   '*** First time hit - create a cookie for the user
   Response.Cookies("wwVisitor")=now

   '*** Open the connection to the RASLOG ODBC database
   Set Conn = Server.CreateObject("ADODB.Connection")
   Conn.Open("dsn=RasLog")
   sql="INSERT INTO wwPageLog (Page, TimeStamp, Browser, Referer, IP, Other)"
   sql=sql + "VALUES ('Default.asp',"

   sql=sql + "datetime(),"
   sql=sql + "'" & Request.ServerVariables("HTTP_USER_AGENT") & "',"
   sql=sql + "'" & Request.ServerVariables("HTTP_REFERER") & "',"
   sql=sql + "'" & Request.ServerVariables("REMOTE_HOST") & "',"
   sql=sql + "'" & Request.Cookies("wwVisitor") & "')"

   ' Response.Write(sql) ' debug

   Conn.Execute(sql)
   Conn.Close
   SET Conn = nothing
end if
%>

<html>

<head>
<title>Logging</title>
</head>

<body bgcolor="#FFFFFF">
<h1>This could be your custom logged homepage</h1>
<hr>
This request has been logged into a VFP database using the ADO ODBC connector.
To view the list of recent requests click on one of the links below:
<UL>
<li><a href="ShowOdbcLog.asp">Show hits on this page...</a>
</UL>
</body>
</html>


'*** ShowODBCLog.ASP - Display the results from the log
<%
   '*** This code always loads the table with the log

   '*** Open the Connection
   Set Conn = Server.CreateObject("ADODB.Connection")

   Conn.Open("dsn=RasLog")

   '*** Retrieve the date from the Input form
   lcDate=Request("LogDate")

   '*** Handle ALL selection
   IF UCASE(lcDate)="ALL" Then
      lcWhere=""
   ELSE
      '*** If Emtpy default to today's date
      IF lcDate="" THEN
        '*** On Empty date show all entries
	    lcDate=cDate(Date)
        lcWhere= "WHERE ttod(timestamp) = ctod('" & lcDate & "')"
       ELSE
		  '*** Otherwise filter to today's date
		  lcWhere="WHERE ttod(timestamp) = ctod('" & Request("LogDate") & "')"

	   END IF
	END IF

   '*** Build the SQL Statement And Execute into a RecordSet
   SQL = "SELECT TimeStamp, Referer, Browser, IP FROM wwPageLog " & _
  		 lcWhere & " GROUP BY 1 ORDER BY 1 DESCENDING"

   Set rs = Conn.Execute(sql)
%>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>

<head>
<title>Show Hits to Default Page</title>
</head>

<body bgcolor="#FFFFFF">
<h2>Show Hits to Default.asp</h2>

<form action="ShowODBCLog.asp" method="POST">
    <p><strong>Date:</strong>
	<input type="text" size="8" name="LogDate" value="<% =lcDate%>" >
    <p>
	<input type="submit" name="btnSubmit" value="Display Log">
	</p>
</form>

<table border="1" cellpadding="3" width="100%" bgcolor="#EEEEEE"
border="3">
    <tr>
        <td align="center" bgcolor="#FFFDCA"><strong>Time</strong></td>
        <td align="center" bgcolor="#FFFDCA"><strong>Referer</strong></td>
        <td align="center" bgcolor="#FFFDCA"><strong>Browser</strong></td>
        <td align="center" bgcolor="#FFFDCA"><strong>IP Address</strong></td>
    </tr>

<% Do While Not RS.EOF  %>    <tr>
        <td><% =rs("Timestamp")%></td>
        <td><a href="<% =rs("Referer")%>"><% =rs("Referer")%></a></td>
        <td><% =rs("Browser")%></td>
        <td><% =rs("IP")%></td>
    </tr>
<%
RS.MoveNext
Loop

'*** Now Calc the total
sql="SELECT COUNT(*) as TotalHits FROM wwPageLog "  & lcWhere
SET rs=Conn.Execute(Sql)
%>    <tr>
        <td align="center" colspan="3" bgcolor="#FFFDCA"
        align="RIGHT"><b>Total number of hits:</b></td>
        <td align="center" bgcolor="#FFFDCA"><b><% =rs("TotalHits")%></b></td>
    </tr>
<%
RS.Close
Conn.Close
%>
</table>
</body>
</html>

External Object Creation with the Server Object

The Server object is used for server configuration and for the creation of Automation objects to extend Active Server and provide interfaces to system services.

One of the most exciting aspects of Active Server is it's ability to instantiate OLE Automation servers directly from within the HTML scripted code. You can use the Server.CreateObject() method to create an object instance to any Automation server available on the server machine.

Figure 1.4 shows how external objects are managed by the server. This is a crucial piece for interfacing Visual FoxPro code into Active Server by allowing the Server object to create and invoke Visual FoxPro Automation servers directly from within an ASP script.

Figure 1.4 - The ActiveX Server framework allows creation of Automation objects from within VBScript. Once created the object can be accessed using Automation server methods and properties the results of which can be displayed in the HTML script page.


The following Active Server page calls a VFP Automation server that implements a counter variable that’s increased on each hit. The IncCounter() method uses VFP code to access and write the registry with the updated counter value. The CustList() method accesses a Visual FoxPro table and generates an HTML table that shows the contents of the table which is returned to the ASP page as a text string result.

<html>
<head>
<title>Active Server Automation</title>
</head>

<body bgcolor="#FFFFFF">

<h2>Visual FoxPro Automation Server called from an Active Server Page</h2>

<hr>

<p>This page has been hit:<strong> <%
   SET oServer = Server.CreateObject("wwWebtools.AspTools")

   Response.Write(oServer.IncCounter("ASPDemoCounter"))

   lcCompany=Request("txtCompany")
   %> </strong>times.</p>

<p>The following table was generated by Visual FoxPro: </p>

<form method="POST">
    <p>Enter a Name: <input type="text" size="24"
    name="txtCompany" value="<% =lcCompany%>"> <input
    type="submit" name="btnSubmit" value="Filter List"></p>
</form>

<p align="center"><% =oServer.CustList( (lcCompany) )%></p>

</body>
</html>

This code is very straight forward. However, in order to really optimize access to Automation servers it’s crucial to persist servers across requests. Without this loadtime for the server is excessive and makes use of Automation servers inappropriate. To persist a server you can use the ASP Session object.

Replace the CreateObject code above with:

IF IsObject(Session("soServer")) THEN
   SET oServer = Session("oServer")
ELSE
   SET oServer = Server.CreateObject("wwWebtools.AspTools")
   SET Session("oServer")= oServer
END IF

The server reference is now active for the duration of the user’s session and access speed to the server is drastically improved.

Automation Server limitations

Be aware of some serious implications of Automation servers that are used with ASP. Active Server wants to work with DLL based Automation objects, but Visual FoxPro will allow you to only use one DLL based server per Web server as VFP only supports a single copy of the runtime in memory of any single process (in this case the Web server). The single copy can only serialize calls to itself, so if you run a long query any other hits on that same server are blocked.

By default ASP doesn't even run with EXE servers – you need to create a registry value and set it to DWORD with a value of 1. Create it as:

\\HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\W3SVC\ASP\Parameters\AllowOutofProcCmpnts

You run can multiple EXE servers at the same time, but be aware that any EXE based Automation server calls will block all other EXE Automation calls at the same time! This has some serious implications on performance as everything is bottled down to a single thread while your server executes. Hence, it's not recommended that you use Automation servers for any operations that are lengthy.

True multi-threaded operation for Automation servers is possible only with Free Threaded servers or those that have a threading model of 'Both'. None of the current high level development tools provides this kind of interface at the moment and you're required to write scalable components directly against the native COM interfaces using low level languages such as C++ or Delphi.

Active Server Summary

Pros:

Cons:

FoxISAPI

Visual FoxPro's new capability to create Automation servers has brought about another slick option for implementing Visual FoxPro based Web applications. What if you could use an Automation server to respond to a request placed from a Web page to handle the data processing and HTML page generation? With a cool called FoxISAPI that's provided by Microsoft with Visual FoxPro you can do just that.

Figure 1.5 - FoxISAPI uses an ISAPI DLL to create an instance of an Automation server and call the specified method in the server.

Connect VFP Automation servers to HTML HREF or form links

FoxISAPI is used by calling the FoxISAPI.dll from an HTML link or form. A typical HTML link looks like this:

HREF="/scripts/foxisapi.dll/Tserver.Tclass.Tmethod?UID=1111&Name=Rick"

ISAPI DLL instantiates persistent Automation Automation object

FoxISAPI works by calling an ISAPI script from an HTML HREF or form link. The FoxISAPI.dll creates an object reference to the object passed as part of the URL and calls the specified method. The object is persistent so repeated access to the object is very quick as the object is never reloaded unless explicitly unloaded (more on that below).

In the example above FoxISAPI parses out the ClassId and method call to instantiate the Tserver.Tclass Automation server (equivalent to doing CREATEOBJ("Tserver.Tclass") in VFP) and then goes ahead and calls the Tmethod() method (equivalent to lcResult = oServer.Tmethod(cParm1,cParm2,nParm3)). FoxISAPI expects your method to return an HTTP compliant result string - in most cases this result will be an HTML document, but could also be an HTTP request for Authentication or Redirection.

Passes form variables as parameter and server variables in an INI file

When FoxISAPI calls your Automation server it passes along 3 parameters to each request method that is called. Your server method that respond directly to request must support these three parameters:

  • Key value pairs are separated by &.
  • Spaces are converted to +.
  • All 'extended' characters are converted to Hex escape codes. The escape code uses a percent sign plus a hex number to store the ASCII code of the characters. For example a carriage return (ASC(13)) is encoded as %0D.
  • The EMPLOYEE example in your \VFP\SAMPLES\SERVERS\FOXISAPI directory includes a decoding algorithm as does the starter FoxISAPI class provided on the DevCon CD (FoxISAPI::DecodeURL).
  • lcIniFile
    This parameter contains the path to an INI file that contains all the server, browser and system variables. You can retrieve these with calls to the GetProfileString API call (or use the CDs FoxISAPI::GetCGIVar(cVarname,cSection)).
  • lnReleaseFlag
    This parameter determines whether the reference to the Automation server will be kept or released. By default it's a good idea to keep the reference to keep a connection in order to minimize load time of the server. The parameter is passed in by reference so changing it in your code will effectively
    0 - Keep Server Reference (default)
    1 - Release Server Reference

Must return HTTP compliant output

Once your code gets control you can use VFP as you see fit to run queries or run any other kind of transaction or logic operation using FoxPro code. The end result of each exit point of your method must be a HTTP compliant string.

In most cases the output will be an HTML document, but you have to be sure to add an HTTP header to the top of it. Output should look like this:

HTTP/1.0 200 OK
Content-type: text/html

<HTML>
<H1>Hello World</H2>
</HTML>

The HTTP header and Content-type are important since not all browsers will support headerless results. Note each line of the header must be separated by a CHR(13)+CHR(10) and the final header line must be followed by a blank line containing only the CHR(13)+CHR(10).

HTTP headers for authentication, redirection, Cookies etc.

While you will almost always return an HTML document it's possible to generate standard HTTP header responses as well. For example, if you wanted to cause Authentication to occur you might pass:

HTTP/1.0 401 Not Authorized
WWW-Authenticate: basic realm=west-wind.com

<HTML>
Get out and stay out!!!
</HTML>

This would cause an authentication box to be thrown up by the browser. You can then check the password entered as passed back in a the Authenticated User CGI variable to determine whether to allow the user in (actually NT will first fail the user if the user is not valid as per the User Manager).

A simple FoxISAPI Server code example

The following code snippet shows the simplest Automation server you can build and run with FoxISAPI. While this is not a very functional example it does show the basic elements required when building methods for responding to FoxISAPI calls.

The script would be called with the following URL HREF link:


   HREF="/scripts/foxisapi.dll/TDevCon.TFoxIsapi.Helloworld?"

 

You'd create an Automation server named TDevCon with the following code:

#DEFINE CR CHR(13)+CHR(10)

DEFINE CLASS TFoxISAPI AS Custom OLEPUBLIC

FUNCTION Helloworld
LPARAMETER lcFormVars, lcIniFile, lnReleaseFlag
LOCAL lcOutput

*** HTTP header - REQUIRED on each request!
*** System Defines
lcOutput="HTTP/1.0 200 OK"+CR+;
         "Content-type: text/html"+CR+CR

lcOutput=lcOutput+;
   "<HTML><BODY>"+CR+;
   "<H1>Hello World from Visual FoxPro</H1><HR>"+CR+;
   "This page was generated by Visual FoxPro...<HR>"+CR+;
   "</HTML></BODY>"

RETURN lcOutput

ENDDEFINE

Note that the HTTP header is created as part of the output and that the output, header and HTML document both, are returned by the function. FoxISAPI takes this output and sends it - as is - back to the Web browser.

Set up for Automation Servers called by Web services

Setting up FoxISAPI is the most tricky part of working with this tool. Especially when running under Windows NT 4.0 special steps have to be taken to make sure Automation servers can be properly created by the Web server process which owns the FoxISAPI process that calls your Automation servers. BTW, the same rules apply to Automation servers called by Denali!

Automation Server must be registered

The first step is to make sure your Automation server is registered on the Web server machine. If you build your project on the same box then the server is already registered for you when VFP built the project.

If you're only copying the Automation server to this machine make sure you run:

Copy FoxISAPI.dll into script dir

Run DCOMCnfg on NT 4.0

On Windows NT 4.0 DCOM configuration is necessary for all EXE servers launched by the Web server process. DLL servers also need to have at least the default rights set to include the Web server user account with launch permissions.

Add IUSR_ account to default rights
The IUSER_Machine account is the user account used by IIS while running a Web request. This account needs to be added to provide default launch and access rights for all OLE servers:

Start DCOMCNFG

Go to Default Security

Add IUSR_ to Default Access Permissions

Set user to Interactive User on the specific server
EXE require that the server is defined for interactive use. To set this:

  1. Go to the Applications Tab
  2. Select the Class (TFoxIsapi in the example)
  3. Click Properties
  4. Go to the Identity tab
  5. Click The Interactive User
  6. Click Apply

Re-run whenever server is rebuilt
Repeat steps 4 - 10 whenever rebuilding the server on the local machine, which rebuilds the Automation Ids and blows away the DCOM settings.

 

FoxISAPI Server Instancing

As with all Automation Servers instancing can have a big effect on how your application performs. For Web applications instancing is even more important a speed is crucial and multiple simultaneous requests need to be processed.

InProcess DLL

MultiUse (Out of Process)

SingleUse (Out of Process) (recommended)

Multiple instances via Pool Manager

Note: You must download the latest version of FoxISAPI.DLL from the MS Web site. VFP 5.0a did not ship with the final build of this tool.

With multiple instances of the same server, requests are served to all server instances as needed. If the first instance is busy the second instance will take the request. If all servers are busy and the pool of servers is exhausted the request is queued. In order to use the internal Pool Manager the server must be single use EXE!

The pool manager built into FoxISAPI's DLL can load multiple instances of your Automation server and access another instance whenever one is busy.

Multiple servers are configured via the FoxISAPI.INI startup config file:

[foxisapi]
busytimeout = 5
releasetimeout=13
statusurl = Status
reseturl= Reset
SingleModeModeUrl = SingleMode
MultiModeUrl = MultiMode
[wwFoxISAPI.TFoxISAPI]
wwFoxISAPI.TFoxISAPI=2
R_FoxIsapi.T_R_Foxisapi=1
[foxis.employee]
foxisr2.employee=2
foxisr1.employee=1
foxis.employee = 2

The first keys in the [FOXISAPI] section determine how you can manage servers. The various URL keys allow you to customize the commands used on the URL to run that command:

foxisapi.dll/status
foxisapi.dll/reset
foxisapi.dll/singlemode

The Status URL displays a list of all servers that are currently loaded. Reset releases all servers. SingleMode releases all servers but the first so you can run maintainenence operations that require EXCLUSIVE access to the data.

The actual Automation servers are configured with a separate section in FoxISAPI.INI. The section serves as a map to translate your ClassID passed on the URL to translate to the actual ClassIDs that you want to call. Since you can call both the same server or a local and remote server. Since locals and remote must have different class ids this mechanism allows you transparently to load the remote with the same URL as the local.

In the [foxis.employee] example above FoxISAPI would first load the FoxISr2 server when starting up. Once this server gets busy it would load another instance since the key value is 2. When both of these are busy foxisr1 would get loaded. Finally, a local copy gets started and two instances of that can be running before requests start queuing.

Typically, you'd want to load the local servers first, but it depends on your server load.

Configuring Remote Servers

Be careful with Remote servers. While they run reasonably fast there are a number of important issues to deal with.

Essentially, in order to build remote servers you need to make sure you build your server with some conditional logic for each location that it'll run in in order to properly point at the data and the Web Server for retrieving HTML templates.

The following issues have to be dealt with:

  • In this setup the Web server is the Client (Web Connection DLL manages multiple client instances of your server) and the remote machine is the Server.
  • If you want to mix local and remote servers you need to create separate EXE files with separate OLE ClassIds for each remote server. This is so DCOM can properly determine which machine to run the server on based on the entries in the registry from the single machine that wc.dll runs on. For example, you might have a local server called wcDemoOle.wcDemoServer. You can then create wcRemote1.wcRemoteServer1, wcRemote2.wcRemoteServer2 etc. In VFP 5 the server name corresponds to the EXE/DLL name and the class corresponds to the OLEPUBLIC class that your code exposes (your wwOLEServer subclass in this case).It's important that the Class names are different or else you will run into conflicts in the DCOMCNFG utility discussed above!
  • Build the server and make sure it's registered on the remote machine. It's best to test the server locally on the remote before attempting to run it over DCOM. Make sure you apply the DCOMCNFG settings described in the previous section. To see the server come up locally try code like the following:

o=CREATEOBJECT("wcRemote.wcRemoteServer")
o.show()

  • Copy the .VBR files created when you build your Automation server to a directory on your Web Server for each Automation server you want to remote. The remote EXE file is not needed on the Web Server.
  • Run CliReg32 (in your \VFP directory) and pass it the name of each of the VBR files. Choose DCOM for the transport and set the appropriate IP address for the remote server! Important: the server must be registered on the remote or load will always fail.
  • At this point you should test the server from the Web server machine. Fire up VFP and see if you can instantiate the remote object on the remote machine. Use the CREATEOBJECT code a few steps back.If the server comes up on the remote box your server is installed correctly for operation under DCOM.
  • On the Remote machine start DCOMCNFG and add the IUSR_ account of the Web server to the Default Access and Launch permissions on the Default Security tab. If you don't use a Domain Server you'll need to explicitly create new account/password and match it to the Web Server's account and password. With IIS the IUSR_ password is a random value so you'll have to change it both in User Manager and in the IIS configuration utility. If you don't want to hassle with the IUSR_ account you can also use the EveryBody group to allow access for all, but keep in mind that this is a potential security risk.
  • You should now be ready to load the server through Web Connection. Add the remote servers to wc.ini:

[wwDemoOle.wcDemoServer]
wwDemoOle.wcDemoServer=2
wcRemote.wcRemoteServer=1

This setting will load two local servers and one remote server.

  • If all is well, the server will come up on the remote box as expected. Note that load time for remote server is significantly slower than local servers as is the time it takes to release the server. Operation of method calls however, is swift. If the server fails to load, you'll see a COM error message returned.
  • For debugging this setup I suggest you log on to your Web Server box as your IUSR_ account then fire up VFP to try and create the remote object. This allows you to interactively change the DCOM settings plus get the COM error messages properly returned to you by VFP. It's much easier to handle and respond to the errors in VPFs interactive environment than trying to handle the Web server and its error messages.

Starter FoxISAPI class provided on the CD

The DevCON CD contains a starter FoxISAPI class that contains a few useful methods for handling typical tasks when handling FoxISAPI requests. This class is by no means extensive, nor does it provide all the features you need, but it's a starting point for writing your own library. For a more complete implementation framework of class you might want to check out Web Connection's OLE connector library.

Following is a list of methods and properties available in the FoxISAPI class:

cOutput			Temporary holding property that contains all
			text output to be returned from the request
			Method
Send/SendLn()		Send  text to output
StandardPage()		Generates a full HTML page
ContentTypeHeader()	Adds HTTP header
StartRequest()		Called to set up a request. Decodes input vars
                        and clears the cOutput property.
GetFormVar()		Retrieves a form variable passed in 				           	                       with the first parameter.
GetCGIVar()		Retrieves a server/browser Variable from
                        the INI file.
ReleaseServer()		Standard method that releases OLE server.

Here's another complete, simple example server that uses most of the above methods (note this method should be in the same class as the example described above – the class header is provided here for completeness of the sample only):


DEFINE CLASS TFoxISAPI AS FoxISAPI OLEPUBLIC

FUNCTION TestMethod
LPARAMETER lcFormVars, lcIniFile, lnReleaseFlag
LOCAL lcOutput

*** Decode the Form Vars and assign INI file to class property
THIS.StartRequest(lcFormVars,lcIniFile)

*** Don't Release the server
* lnReleaseFlag=0       && 0 - Don't release (default)  1 - Release

*** Must always add a content Type Header
THIS.HTMLContentTypeHeader()

*** Grab HTML Form Variables
lcUserId=THIS.GetFormVar("UserId")
lcName=THIS.GetFormVar("UserName")

*** Start HTML Generation
THIS.SendLn("<HTML><HEAD><TITLE>Hello from FoxISAPI</TITLE><HEAD><BODY>")
THIS.SendLn("<H1><FONT=Arial COLOR=#800000>Hello World from Visual FoxPro</FONT></H1>")
*THIS.SendLn("This page was generated by Visual FoxPro using FOXISAPI. ")
*THIS.SendLn("The current time is: "+time()+"<p>")

THIS.SendLn("<b>Encoded Form/URL variables:</b> "+lcFormVars+"<BR>")
THIS.SendLn("<b>Decoded UserId:</b> "+ THIS.GetFormVar("UserId")+"<br>")
THIS.SendLn("<b>Decoded UserName:</b> " +THIS.GetFormVar("UserName")+"<P>")

*** Show the content of the FOXISAPI INI server/browser vars
IF !EMPTY(lcIniFile) AND FILE(lcIniFile)
   CREATE CURSOR TMemo (TFile M )
   APPEND BLANK
   APPEND MEMO TFile from (lcIniFile)
   THIS.SendLn("Here's the content of: <i>"+lcIniFile+;
     "</i>. You can retrieve any of these with <i>THIS.GetCGIVar(cVarname,cSection)</i>:<p>")
   THIS.SendLn([THIS.GetCGIVar("HTTP_USER_AGENT","ALL_HTTP"): ]+;
                THIS.GetCGIVar("HTTP_USER_AGENT","ALL_HTTP"))

   THIS.Send("<PRE>")
   THIS.SendLn(Tmemo.Tfile)
   THIS.SendLn("</PRE>")
   USE in TMemo
ENDIF

THIS.SendLn("<HR></HTML></BODY>")

RETURN THIS.cOutput

ENDDEFINE

FoxISAPI Summary

Pros:

Cons:

West Wind Web Connection

FoxISAPI provides basic Web connectivity features that are relatively easy to build using Visual FoxPro as a back end server. Web Connection expands on the basic functionality of FoxISAPI by providing an entire application framework and set of tools that make it easy to build Web based backend applications in Visual FoxPro.

Extensive Visual FoxPro framework for Web development

Web Connection provides host of functionality that is specific to Web development. The Web Connection framework provides a number of predefined objects that greatly simplify the process of Web application development by abstracting all the Web specific logic into class libraries so you can focus on your application and not on how to get and output information to and from the Web server.

Applications are built in an application class that has built-in objects for retrieving server and HTML form variables from the Web server and an extremely powerful and flexible HTML class that allows for on the fly HTML HTML

Creation using both low level and high level methods. For example, you can display data with a single call to the ShowCursor() and output the current table to HTML. HTML based scripting with Visual FoxPro code is available via the ShowHTMLFile() method which allows embedding of Visual FoxPro expressions and even entire code blocks directly into the HTML to be displayed. Or if you need low level control you can use the Send() method to output text directly to the server. The HTML class contains over 50 methods for helping you create HTML quickly and providing full control over the HTTP output including built-in support for HTTP Headers, Cookies, graphing via ActiveX…

The application framework also provides solid error handling and reporting, parameter parsing, displaying HTML pages from disk via script mapping, form variable decoding, sending SMTP email, FTP and domain name lookup and much more.

Simulated multi-threading with multiple VFP sessions

Web Connection natively supports running multiple instances of any Web Connection server either on the local machine or across the network for maximum scalability. Web Connection has been clocked at close to 200,000/day Visual FoxPro backend hits using two instances of Visual FoxPro Automation servers running on a live commerce site (http://www.surplusdirect.com/). Up to 450,000 hits have been tested in an offline testing environment and even that hasn’t maxed out a single machine (Single processor Ppro 200 with 64 megs – average request time: 0.12 secs).

Under OLE Automation the Web Connection DLL manages multiple persistent instances of the same Automation server and services requests round robin optimizing throughput through servers. With file based messaging multiple EXE’s or standalone applications can be started and run transparently against the same set of data. Scaling up with multiple server instances is totally transparent to the implementation!

Why run multiple servers? Visual FoxPro doesn't support multi-threading but you can simulate this feature by running multiple Visual FoxPro sessions each running a copy of the Web data server. Multiple servers typically don’t enhance overall performance, but they can provide better responsiveness to the users of your site by making it possible to process short requests

A single processor can handle two sessions without much overhead, although more session than that will start showing drastic slowdowns as the CPU load gets too large with the simultaneous queries. Multi-processor machines allow additional sessions to scale closer to 90% per session throughput that is acceptable.

Scalable across multiple machines over the network

Web Connection can also scale across the network to further expand processing power if you've maxed out the local processor(s). Web servers are multi-threaded and are fairly CPU intensive especially on single processor machines. Moving processing off the local machine to a network machine frees up the Web server for providing better response which is important for keeping sites running smoothly.

Keep in mind though that scaling over the network incurs a slight performance hit. Local disk access is usually much quicker than requests and result pages travelling over the network. Still, in combination with local data servers, remote servers can greatly enhance the Scalability of a site.

Online code updates without shutting down Web server

Web Connection supports live code updates by providing an update mode for Automation servers that hold all requests and then update the Automation server EXE from an update path. Files can be uploaded to the server via the update path and then hot swapped via an HTML link.

Real time, live debugging

Like FoxISAPI Web Connection leverages ISAPI and OLE Automation, but Web Connection also supports a file based ‘interactive’ mode that can run on non-ISAPI Web servers as well as providing a real time ‘live’ development environment inside an interactive Visual FoxPro session, so you can debug your server in real time from real Web requests.

You can receive a request from the Web server and set a breakpoint and step through the code when the request hits. This allows you to test in a live environment where you see the actual hit incoming from the Web server rather than depending on simulated test data manually passed to the server.

The code, compile, debug, fix code cycle is also much quicker since you can fix errors at the source when the code crashes where you can immediately fire up the debugger and examine the status of the application.

Server Management

One of the strengths of Web Connection is that the framework is geared towards helping automating server addministration. Web Connection can automatically log each incoming Web Request and provide both HTML and VFP based viewers for the data. Sessions can be managed both via an HTML based administration page or via settings in an INI file for startup settings. It’s possible to start and stop servers remotely and get information on which servers are running and their load. In addition maintaincence modes are supported so all but one server can be shut down to allow exclusive access to data.

How Web Connection works

Web Connection’s approach to creating Web application is similar to FoxISAPI’s although the overall implementation is a little more complex as the framework uses a number of interrelated objects to provide the additional functionality described above.

With FoxISAPI your interface to the FoxPro application is direct: You call a method in an Automation server directly. While simple this also means that each and every method call in FoxISAPI needs to call various methods/functions on each hit to provide form variable parsing, header creation, set up for HTML output etc.

Web Connection expands on that scheme by having a generic server object (which is the OLEPUBLIC object) take each Web hit and delegating it to the proper program/class. Each hit creates a new class and calls a pre-defined method (Process()). By doing so all the Web specific processing is not splattered into the application code but handled by the generic server object. You get a central entry point that allows you to hook logic that applies to every Web request.

The actual method to handle your request is then parsed out of the URL command line. A typical HREF link looks like this:

HREF="wc.dll?MyPrg~MyMethod"

Figure 1.6 - The Web Connection data server uses the framework classes to call a method in a CGIProcess object. Adding request handlers is as easy as adding a method to the class.

The CGIProcess object which contains the custom code methods creates both a CGI and HTML object when created and preconfigures these for immediate use. A typical program file that contains the logic to create a new process object and response method looks like this:

*** This routine creates the object and calls the Process() method
PROCEDURE MyPRG
LPARAMETER loCGI
#INCLUDE WCONNECT.H
#DEFINE HTMLPAGEPATH "h:\http\wconnect\"
#DEFINE DATAPATH "wwdemo\"

*** Now create a process object
loCGIProcess=CREATE("webConnectDemo",loCGI)
IF TYPE("loCGIProcess")#"O"
   *** All we can do is return...
   WAIT WINDOW NOWAIT "Unable to create CGI Processing object..."
   RETURN .F.
ENDIF

*** Call the Process Method that handles the request
loCGIProcess.Process

RETURN


*** Application Class Implementation
DEFINE CLASS DevConDemo AS wwCGIProcess   && Baseclass Custom

FUNCTION CustomerList
LOCAL loCGI, loHTML

*** Easier reference
loCGI=THIS.oCGI
loHTML=THIS.oHTML

*** Retrieve the name the user entered - could be blank
lcCustname=loCGI.GetFormVar("Search")
lcBrowser=loCGI.GetBrowser()    && Retrieve Browser name/User Agent

*** Get all entries that have time entries (expense=.F.)
SELECT tt_cust.Company ;
   FROM TT_Cust ;
   WHERE tt_cust.company=lcCustname ;
   ORDER BY Company ;
   INTO CURSOR TQuery

IF _TALLY < 1
   *** Return an HTML response page
   *** You can subclass ErrorMsg to create a customized 'error page'
   THIS.ErrorMsg("No Matching Records Found",;
                 "Please pick another name or use fewer letters "+;
                 "to identify the name to look up<p><HR>")
   RETURN
ENDIF

*** Create HTML document header
*** - Document header, a Browser title, Background Image
loHTML.HTMLHeader("Customer List","Web Connection Customer List",;
                  "/wconnect/whitwav.jpg")

loHTML.SendLn("<b>Returned Records: "+STR(_TALLY)+"</b>")
loHTML.SendPar()
loHTML.SendLn("<CENTER>")

&& Set Display to table or <PRE> list - Optional
loHTML.SetAllowHTMLTables(loCGI.IsHTML30())

*** Show entire result set as an HTML table
loHTML.ShowCursor()

*** Center the table
loHTML.SendLn("</CENTER>")

loHTML.HTMLFooter(PAGEFOOT)

USE IN TQuery
ENDFUNC
* EOF CustomerList
	    FUNCTION MyMethod2
            ...
            ENDFUNC 
            ENDDEFINE 

Rather than passing all parameters explicitly the wwCGIProcess object - the class DevConDemo is derived from - the CGIProcess object contains an embedded CGI object property named oCGI. This object property encapsulates all the information passed by the Web server as contained in the INI content file that is created by the ISAPI dll. The format of the INI file is similar to the FoxISAPI implementation, but it follows the Windows CGI specification rather than using a proprietary format used by FoxISAPI.

The CGI object encapsulates the CGI variables and provides predefined methods for the most common server variables. Here are a few: GetBrowser, GetServerName, GetPreviousURL, IsSecure, IsHTML30, GetCookie, SetOLEClass just to name a few. You can also parse out 'parameters' from the URL using the GetCGIParameter method which takes a numeric argument to return an entry from the Querystring. For example, if you have wwcgi.dll?MyPrg~MyMethod~Parm1~Parm2 and you wanted to retrieve the Parm2 parameter you'd use loCGI.GetCGIParameter(4) to retrieve the value ("Parm2" in this case).

The CGI object also retrieves form variables that were entered on HTML forms. To retrieve form variables you simply call GetFormVar("UserId") to retrieve a form variable by the name of UserId. Multiple selections are parsed into an array and can be retrieved with loCGI.GetFormMultiple(aValues,"MultiFieldname").

In order to simplify HTML output an HTML class is provided that abstracts the output mechanism. As with the CGI object an HTML object property is automatically created with the CGIProcess object when your processing routine is called. It can be referenced as THIS.oHTML and is already set to output to the appropriate output device. The HTML class provides many high and low level methods to handle output of HTML.

At the low level the Send() method is used for ALL output including any output sent by the internal methods. This is done so you can change HTML output mechanism without changing any code. The HTML class supports output to file, string and optimized string (dumps to file when the buffer gets too big which can drastically speed string processing on large strings).

Many high level methods of the HTML class make short work of creating HTML under program control. You can see a few examples in the code snippet. HTMLHeader() creates a starter page header including HTTP header, Title and a standard HTML header. The CGIProcess Class' ErrorMsg() method creates a freestanding HTML page with a single method call. Simply pass a header and message text to be displayed and a full page is sent back to the Web server.

ShowCursor() is a very handy method for displaying an entire table/cursor as an HTML table with a single call. ShowMemoPage() can display HTML documents stored on disk or in a memo field. The pages can also contain embedded Visual FoxPro character expressions which are expanded as the page is loaded and sent back to the Web server for display. This feature is very handy and important as you can use standard HTML editors to create your HTML layout, then simply embed fields and characters variables, and even entire expressions and code blocks directly into the page. Here's an example:

##FOXCODE
PUBLIC wcClientName
wcClientName=UPPER(poCGI.GetFormVar("ClientName"))
SELECT Company, Phone FROM TT_CUST ;
  WHERE upper(company)=wcClientName ;
  INTO Cursor Tquery ORDER BY COMPANY

*** Always return a string
RETURN ""##

<HTML>
<HEAD><TITLE>Client List</TITLE></HEAD>
<BODY BACKGROUND="/wconnect/whitwav.jpg">
<H1>Queried for: ##wcClientName## </H1>
<HR>
First Record Company: ##Tquery.company##<BR>
  First Record Phone: ##Tquery.Phone##<p>
<CENTER>

<!-- HTML Method to display a table in full -->
##poHTML.ShowCursor(,,,.T.)##

</CENTER><HR>

</BODY></HTML>

##FOXCODE
*** Cleanup Code -Always release PUBLIC VARIABLES
RELEASE wcClientName
USE IN TQUERY
*** MUST ALWAYS RETURN STRING!
RETURN ""
##

Any character expressions can be embedded within the ## pairs. If the expression contained within is invalid an error message is embedded in the document instead. Note the call to poHTML.ShowCursor() - UDF() calls are valid and the poHTML object is available in ShowMemoPage() parsed pages to help with inline HTML generation.

##FOXCODE can be used to embed entire code blocks into pages - however, doing so is rather slow and not recommended - it's much more efficient to create a separate UDF() and call it from the page rather than using the inline code block. Expressions work with all versions of Visual FoxPro but the FOXCODE option requires the Development version.

Conclusion


Powered by Web Connection Best viewed with Internet Explorer This page has been visited times since November 9, 1996
Last updated: 02/10/97
[Back to
West Wind Technologies]