★ wanayoo — archive 1999 http://developer.iplanet.com/viewsource/malmskog_nas/malmskog_nas.htmlNouvelle recherche | Portail wanayoo
iPlanet

You are here:  Home > Developers > View Source Articles > Application Server View Source Article
Application Server View Source Article
 iPlanet Developers


Developer Program
  Membership
  One-to-One Support
  Newsgroups
  Member Services

Developer Publications
  View Source
  Developer News

Documentation
  Technical Manuals
  White Papers
  TechNotes
  Sample Code
  FAQs
  Books

Technologies
  Application Server   CSS
  DOM
  CORBA
  Directory & LDAP
  Dynamic HTML
  Java
  JavaScript
  Linux
  RDF
  Security
  SSJS
  XML
  XUL

Developer Downloads
  Tools & SDKs
  Patches

iPlanet Products
  Technical Resources










spacer
Designing an NAS Application

By Steve Malmskog


Send comments and questions about this article to View Source.  
Click here for printer-friendly version

Whenever a development organization is faced with the task of developing a large-scale application within a new application framework, it can be a bit daunting to determine how the software should be laid out architecturally. Should there be a single tier or multiple tiers? If multiple tiers, how many, and which software entities should reside in which tier? When developing your first large-scale software project within Netscape Application Server (NAS), these are just a few of the questions that surface. Along with the rest of the Internet Shopping Network (ISN) development team, I faced these and other similar questions when designing our First Auction Web site in early 1997. At that time, there were no other large-scale NAS sites in existence. The uncharted territory, as we were to quickly discover, was full of potential pitfalls.

In looking back on our development of the First Auction site and reflecting on the design choices we made, I've come to realize that many aspects of the day-to-day development we do now -- particularly how quickly we're able to deploy new features and enhancements -- are a direct result of the design decisions we made back then. In this article, I'll share with you the high-level architectural decisions we made in developing the First Auction site. The specific areas I'll touch on include:

  • breaking NAS into three layers (application, service object, and database)
  • creating a class hierarchy for AppLogics
  • creating a custom AppLogic base class
  • writing your own COM-like objects

While my code examples will be in C++ (and some parts of the discussion will apply only to C++), the same design can just as easily be created with the Java language. I'm going to assume that you are familiar with the basic NAS architecture (NSAPI plug-ins, KJS/KXS/KCS processes, AppLogics, and so on) and have some experience in software development (although minimal) within the NAS framework.

INTRODUCING THE THREE LAYERS

One of the first decisions -- and probably the most significant one -- that we made in designing the First Auction site was to break the NAS business logic into two layers. The general NAS architecture, which is basically a three-tiered design, ends up looking like the diagram in Figure 1.


Figure 1. General three-tiered architecture using NAS


If we exclude the client and focus on the server side, we end up with three layers to work with: the application layer, the service object layer, and the database layer. These three layers comprise the complete scope within which the Web site's software system exists.

Looking at the NAS model in more detail, we can see how our three layers fit into a large-scale NAS environment. In general, the NAS environment is made up of several KJS and KCS processes running on one or more machines. Requests come to the Web server and are dispatched to a KXS via an NSAPI plug-in. Each KJS or KCS receives requests from the KXS and, based on the request, binds the appropriate AppLogic to an available thread in its local thread pool. The AppLogic is then executed in its own thread (possibly accessing the database, file system, and so on) and streams HTML back to the user (usually in the form of a merged template via the EvalTemplate() call).

Figure 2 shows a high-level view of the server-side architecture with our three layers intact. The role played by each of these layers is as follows:

  • Application layer -- Objects (that is, AppLogics) fulfill a particular application requirement (business logic). They very often correspond one-to-one with the pages on a Web site.
  • Service object layer -- Objects in this layer fall in one of two categories: Netscape extensions, which provide either a particular service or a point of access to the database; or stand-alone (COM or non-COM) objects, which serve some common utilitarian purpose.
  • Database layer -- This layer includes the data model, triggers, stored procedures, and so on, as required by the Web site.

Figure 2. Three-layer server-side architecture using NAS


From a software development standpoint, the application layer and service object layer house the bulk of the business logic and services; we'll focus our discussion on these two layers.

THE APPLICATION LAYER

Within the application layer resides all the core business logic to run a Web site. The primary intent of this layer is to provide application-specific logic -- not generic services to be used by other applications. Thus, the business logic for the Web site is encapsulated into individual AppLogics.

Tip: Since there is often a one-to-one correspondence of business logic to a Web page on a site, there's usually a one-to-one correspondence of a Web page to an AppLogic "powering" that page. If you have an existing page-based mockup (or storyboard) for a Web site, this can serve as a rough gauge of how many AppLogics you'll need to develop.

It's very tempting when starting a new project to jump right in and start cranking out one AppLogic for each page being designed. But, while this is a good rule of thumb, you should realize (as we did) that there may be common functionality across many of the AppLogics. Rather than cut and paste this functionality from AppLogic to AppLogic, it makes better sense to develop a class hierarchy based on the common functionality.

In general, the vast majority of AppLogics you'll develop will fall into one of these three categories:

  • Create a page of information merged from the database.
  • Handle data coming in from an HTML form and update or insert the information into a database or the file system.
  • Report information back to a user based on some query.

With these categories in mind, we can create a class hierarchy that corresponds to the AppLogic types as shown in Figure 3. The three subclasses of the CISNAppLogic are CISNPageAppLogic for returning merged page information, CISNFormAppLogic for handling HTML forms, and CISNReportAppLogic for reporting back to the user.


Figure 3. Subclassing the GXAppLogic class by AppLogic functionality


An Example Core Base Class Function

From the base class CISNAppLogic, we would expect components that either provide common convenience functionality or establish some type of standardization for accessing other parts of our system (like extensions) or for event handling. These might include the following:

  • an error-handling mechanism for both system- and user-level errors
  • access and loading of specific NAS extensions
  • SSL secure/insecure transitioning
  • simplified query execution

We'll look at simplified query execution as an example. Usually, the technique for making a simple flat (nonhierarchical) query goes something like this:

  1. Get a database connection (CreateDataConn()).
  2. Get a query object (CreateQuery()).
  3. Load a query into the query object (LoadQuery()).
  4. Execute the query (pDataConn->ExecuteQuery()).
  5. Get back an IGXResultSet from the Execute() call.

In C++, this translates to code like that shown in Example 1. 


Example 1
GenericAppLogic::Execute()
{
   // ...Do something useful...

   // Get the database connection.
   IGXValList* pList = GXCreateValList();

   GXSetValListString(pList, "DSN", "datasource");
   GXSetValListString(pList, "DB", "database");
   GXSetValListString(pList, "USER", "username");
   GXSetValListString(pList, "PSWD", "password");

   IGXDataConn* pDataConn = NULL;
   CreateDataConn(0, 0, pList, m_pContext, &pDataConn);

   // Set up our database parameter marker values.
   IGXValList* pParmList = GXCreateValList();
   GXSetValListString(pParmList, "PARM1", "VALUE1");

   // Get the query object.
   IGXQuery* pQuery = NULL;
   LoadQuery("queryfile.gxq", "myQuery", 0, pParmList, &pQuery);

   // Execute the query and populate the result set.
   IGXResultSet* pRS = NULL;
   pDataConn->ExecuteQuery(0, pQuery, NULL, NULL, &pRS);

   // ...Handle result set...
}

We've left out the error handling for ease of readability, but obviously we should be checking the HRESULT value of each call to ensure that it's GXE_SUCCESS, and handle the error accordingly if it's not. At any rate, you can see that a lot of code gets written for each database access. It would be nice if we could bundle this into a single function and place it in the CISNAppLogic class. We can do this by analyzing the function calls made in terms of the input and output parameters:

  • Input parameters:
    • Connection parameters (a ValList consisting of the data source name, database name, username, and password)
    • Database parameter marker values (a ValList)
    • Query file name
    • Query name
  • Output parameters:
    • Result set

The transient components are the database connection (pDataConn) and the query object (pQuery). The connection parameters ValList is something that for the most part will be fairly static; it would make sense to move this information into a configuration file and access it once (perhaps during AppLogic creation), storing it locally in member variables.  But for now we have code that looks like Example 2.


Example 2
class CISNAppLogic {

public:
   ...

protected:
   IGXValList* pDBParmList;

private:
   ...
};

CISNAppLogic::CISNAppLogic() 
{ 
    pDBParmList = GXCreateValList(); 

    // Read connection parameters from config file and store values in our connection 
    // parameters ValList. 
    LPSTR dbSource; 
    LPSTR dbName; 
    LPSTR dbUsername; 
    LPSTR dbPassword; 

    readConfigVariable(DB_SOURCE, dbSource); 
    readConfigVariable(DB_NAME, dbName); 
    readConfigVariable(DB_USERNAME, dbUsername); 
    readConfigVariable(DB_PASSWORD, dbPassword); 

    GXSetValListString(pDBParmList, "DSN", dbSource); 
    GXSetValListString(pDBParmList, "DB", dbName); 
    GXSetValListString(pDBParmList, "USER", dbUsername); 
    GXSetValListString(pDBParmList, "PSWD", dbPassword); 
}  

Since we have the connection parameters during the AppLogic's construction, we can refer to them whenever we need to. Now we can create a function that will execute a flat query (Example 3).


Example 3
class CISNAppLogic {

...

public:
   STDMETHOD(ExecuteQuery) (
       /* [in] */  LPSTR         queryPath,
       /* [in] */  LPSTR         queryName,
       /* [in] */  IGXValList*   queryParms,
       /* [out] */ IGXResultSet* pRS,
       /* [in] */  IGXTrans*     pTrans=NULL
   );

...
};

CISNAppLogic::ExecuteQuery(LPSTR queryPath, LPSTR queryName, IGXValList* queryParms, 
IGXResultSet* pRS, IGXTrans* pTrans) 
{ 
    // Get the database connection (use our previously constructed pDBParmList). 
    IGXDataConn* pDataConn = NULL; 
    CreateDataConn(0, 0, pDBParmList, m_pContext, &pDataConn); 

    // Get the query object. 
    IGXQuery* pQuery = NULL; 
    LoadQuery(queryPath, queryName, 0, queryParms, &pQuery); 

    // Execute the query and populate the result set. 
    pDataConn->ExecuteQuery(0, pQuery, pTrans, NULL, &pRS); 

    pDataConn->Release(); 
    pQuery->Release(); 
}

Here we've added an IGXTrans pointer to the function's signature so that we correctly support database transactions. You'll notice that the code is similar to the segment we originally wrote in GenericAppLogic's Execute() method (Example 1), but we're now using our convenient connection parameters list (pDBParmList), a single point for retrieving information about the database. Revisiting the AppLogic's Execute() method, we get code that looks like Example 4. Obviously, this gives us a much simpler and cleaner interface for performing query execution.


Example 4
GenericAppLogic::Execute()
{ 
    // ...Do something useful... 

    // Set up our database parameter marker values. 
    IGXValList* pParmList = GXCreateValList(); 
    GXSetValListString(pParmList, "PARM1", "VALUE1"); 

    // Execute the query (no transaction used). 
    IGXResultSet* pRS = NULL; 
    ExecuteQuery("queryfile.gxq", "myQuery", pParmList, pRS); 

    // ...Handle result set... 
}

An Example Subclass

Similar to the example above, we can create custom functionality for each of our three subclasses. As an example, let's look at CISNFormAppLogic. Any AppLogic that takes information from an HTML page may need to perform some kind of validation on the information being retrieved. While much of this can be done with client-side JavaScript, there may be cases where server-side validation is the only viable option (any validation that requires a database access, for example). Let's assume, though, that we want CISNFormAppLogic to be able to perform rudimentary data type validation. This would make it unnecessary for us to have every AppLogic that pulls information from an HTML form do its own version of validation and possibly generate inconsistencies in our validation logic. In this case, we might find ourselves with an API like the one shown in Example 5.


Example 5
class CISNFormAppLogic : public CISNAppLogic {

public:
   ...

   STDMETHOD(validateFormItem) (
       /* [in]  */ LPSTR formValue,
       /* [in]  */ LONG  dataType,
       /* [out] */ BOOL& isValid
   );

   ...
};

The various data types can be defined as constants, such as FORM_DATATYPE_DATETIME. We would pass in a value from the form along with its data type and then determine whether it's valid. If the value turns out to be invalid, we might return the error to the user via a standardized user-level error handler (which could be localized to CISNAppLogic so that all applications can make use of it). The code might look something like Example 6.


Example 6
class CISNMyFormAppLogic : public CISNFormAppLogic {
   ...
};
CISNMyFormAppLogic::Execute()
{
   // ...Do something useful...
   BOOL isValid = TRUE; // Be an optimist.
   validateFormItem(GXGetValListString("DATE_OF_BIRTH"), FORM_DATATYPE_DATE, isValid);
   if (!isValid) {
       // Use some standardized error handler for user-level errors.
       addErrorMsg(ERROR_MSG_INVALID_DATE_OF_BIRTH);
   }
   validateFormItem(GXGetValListString("AGE"),FORM_DATATYPE_INTEGER, (isValid = TRUE));
   if (!isValid) {
       addErrorMsg(ERROR_MSG_INVALID_AGE);
   }
   ...
   if (hasErrors()) {
       // We have errors, so show them to the user and resend the same template back.
       return handleErrors(HTML_FORM_TEMPLATE_PATH);
   }
   ...
} 

From Example 6 you can see how we can easily take advantage of localized data type validation for any AppLogic that gathers data from an HTML form. Additionally, using a common error-handling mechanism simplifies how the errors are returned to a user. In this case, we've come up with a function, addErrorMsg(), that takes a error code parameter. This parameter may be associated with an actual error string (in a flat file or in the database) that's appended to an error list. Once we've added the error message to our list, we can set a flag indicating that an error condition exists. A call to hasErrors() by an AppLogic will test for the existence of an error in the message list. If one exists, we can call handleErrors(), which would reevaluate the same template the user previously had (the HTML form), but this time with the errors listed at the top. We can use the m_pValIn list to repopulate the form with the originally entered data so that the user doesn't have to retype everything.

Be aware that while most AppLogics will fall neatly under one of the categories mentioned earlier, you'll always run across the need for AppLogics that aren't necessarily based on user events. Electronic commerce sites are full of batch jobs that must be run at periodic times (settling credit card transactions, for example). In cases like these, your AppLogics will follow a model that closely resembles a Unix-based cron job. Fortunately, NAS supports such a model through the use of the IGXAppEvent interface. Events are registered with NAS and will perform AppLogic execution, e-mail transmission, or both. The time at which the events will occur is determined when the event is registered.

Note that because the registration and modification of events with the AppEvent manager is API-based, it's wise to write an AppLogic-based tool with an HTML front end that can act as an administrative tool for this API. Site maintainers can use this tool to register and modify events. Realize that as you develop a site, you'll come across more and more instances of "infrastructure" AppLogics that are over and above the one-page-to-one-AppLogic estimate you may make when developing a Web site. Additionally, you'll have these "cron job"-type AppLogics that will also raise your AppLogic development estimate.

THE SERVICE OBJECT LAYER

Now that you have a feel for what belongs in the application layer and how to structure the applications within that layer, we'll take a look at the far more daunting task of writing classes for the service object layer. Software for this layer should attempt to accomplish one (or more) of the following goals:

  • Perform some common utility for applications or other services.
  • Provide some "service" for applications, with or without accessing the database.
  • Provide an access point for applications (that is, AppLogics) to select, update, or delete information from the database.

If your software component isn't targeted to accomplish one of the above goals, it should most likely be written in the application layer.

For those software pieces that accomplish the aforementioned goals, there are specific techniques for developing each type of component. We'll explore the first goal (utility objects) as an example to give you a feel for what it takes to develop service objects.

COM Objects in NAS

NAS provides an open-ended mechanism for implementing objects that are used in a "service" capacity by the application layer. This is accomplished by creating a class with a COM (Component Object Model) interface and then using a concrete implementation class to fulfill the requirements of the interface. A class is considered "concrete" if it does not contain any pure virtual functions. An interface in C++ is really just an abstract class; that is, it contains one or more pure virtual functions, and as such cannot be instantiated. The combination of using an abstract class (interface) with a concrete one (implementation) is commonly referred to as the "interface/implementation" idiom.

All NAS COM objects inherit from an object provided by Netscape: IGXObject. IGXObject is really just an empty class that has been inherited from the abstract base class, IUnknown. IUnknown contains the following pure virtual functions, which provide the core functionality common to all COM objects:

  • QueryInterface() tests an object to see whether it supports a certain interface, and if so, dynamically casts the object to use that interface. In C++ parlance, it tests to see whether an object has inherited from a particular abstract class, and if so, binds an abstract class pointer to that object so that the interface can be used.
  • AddRef() and Release() support a reference-counting model for memory management. AddRef() increments the reference count and Release() decrements it. When an object's reference count goes to 0, it's deleted off the heap. With this type of model in place, objects are not directly deleted with the C++ delete keyword. For example:
  • IGXValList* pValList = GXCreateValList();
    pValList->Release();   // Correct
    delete pValList;       // Error

Because all our COM objects inherit from IGXObject, they must implement, as a minimum, these three functions.

Additionally, every COM object's interface must have a unique identifier that we can use when we're testing an object to see whether it supports a particular interface. This unique identifier is known as a GUID (globally unique identifier), or alternately an IID (interface identifier). (Note that Netscape has used the C++ typedef mechanism to make both a GUID and an IID identical in the gxdefs.h file.) Thus, whenever we create a new COM object within our service object layer we need to create a new IID. Fortunately, NAS provides us with a command-line utility, kuuidgen, to do just that.

Once we have an IID, we need an IDL file for our COM object. This file specifies the interface that's associated with our IID. Example 7 is a segment of the abstract class header file (IGXTemplateMap.h, generated from an IDL file via the KIDL utility) for the IGXTemplateMap interface.


Example 7
#include "IGXObject.h"

struct IGXObject;
struct IGXBuffer;

EXTERN_C const IID IID_IGXTemplateMap;

// [local, object, uuid(16FB0740-9A3C-11cf-970E-0020AFED9A65)]

struct IGXTemplateMap : public IGXObject
{

// IGXTemplateMap is used during template processing to do field mapping.
public:
   virtual HRESULT STDMETHODCALLTYPE Get(
       /* [in] */  LPSTR                           szExpr,
       /* [in] */  IGXObject __RPC_FAR*            pData,
       /* [in] */  IGXObject __RPC_FAR*            pMark,
       /* [out] */ IGXBuffer __RPC_FAR* __RPC_FAR* pBuff) = 0;
};

You'll notice the interesting line

EXTERN_C const IID IID_IGXTemplateMap;

What's going on here? Well, we're declaring that externally defined (somewhere!), there is a constant, IID_IGXTemplateMap, that corresponds to this interface. So, if we ever wanted to test an object to see whether it's using this interface, we'd use IID_IGXTemplateMap.

Note: That "somewhere" where the IID_IGXTemplateMap IID is defined is actually the NAS library libgxidl.so. This library contains all the IIDs for the various NAS-included COM objects. Thus, whenever you compile a C++ AppLogic you'd link in this library.

Looking at the IGXTemplateMap interface in Example 7, we see that it inherits from IGXObject and makes a single API call, to Get(), which takes four arguments. The interface is a pure virtual function (notice the "= 0"). If we wanted to inherit from this class, we'd have to implement not only QueryInterface(), AddRef(), and Release() (to fulfill our obligation to IGXObject), but also Get() (from IGXTemplateMap).

An Example Utility Class

Suppose we want to create a utility class, CISNTemplateMap, that we might use all over our Web site -- in anything from applications to other service objects. This is an excellent candidate for inclusion in the service object layer, as it would fulfill the first goal of performing some common utility for applications or other services. Let's say CISNTemplateMap is going to extend the IGXTemplateMap functionality so that we can put items into the map with Put() in addition to just getting them with Get(). Thus, we'd want to create an interface for CISNTemplateMap -- say, IISNTemplateMap. (I follow a convention of preceding interface names with "I" and names of concrete classes with "C".) This would give us a file structure like the one shown in Figure 4.


Figure 4. File entities and their class counterparts for NAS COM object implementation


From Figure 4 you can see that a COM object requires four files:

  • IDL file -- an IDL definition of the interface we want to specify
  • IDL header file -- the language-specific header file, which results from running the IDL file through an IDL compiler (KIDL, for NAS)
  • Implementation header file -- The implementation classes' header file, which contains the non-pure virtual function declarations for the class
  • Implementation code file -- The *.cpp file, which contains the actual implementation code

For IISNTemplateMap, a segment of the IDL file might look like Example 8.


Example 8
interface IISNTemplateMap;

[
 local,
 object,
 uuid(A2D9167A-D1B6-1446-B6F0-08002080A910)
]

interface IISNTemplateMap : IGXTemplateMap
{
   STDMETHOD(Put) (
       [in] LPSTR theKey,
       [in] LPSTR theValue
   );
}

Note that we have an IID already specified for this interface along with the interface's API (in this case, just the single call, Put()). When this is compiled with KIDL, we get something like Example 9 as output.


Example 9
class IISNTemplateMap;

/* [
 local,
 object,
 uuid(A2D9167A-D1B6-1446-B6F0-08002080A910)
] */

extern "C" const IID IID_IISNTemplateMap;

class IISNTemplateMap : public IGXTemplateMap {

public:
    STDMETHOD(Put) (
        /* [in] */ LPSTR theKey,
        /* [in] */ LPSTR theValue
    );
}; 

By running the file through the IDL compiler, we've generated a C++ abstract class with Put() as a pure virtual function. We can now #include this file in our implementation header file (Example 10).


Example 10
#include "IISNTemplateMap.h"   // Our interface

class CISNTemplateMap : public IISNTemplateMap {

public:
    CISNTemplateMap();
    virtual ~CISNTemplateMap();

    // IUnknown Interface
    STDMETHOD(QueryInterface) (REFIID riid, LPVOID *ppvObject);
    STDMETHOD_(ULONG, AddRef) ()
    STDMETHOD_(ULONG, Release) ();

    // IGXTemplateMap Interface 
    STDMETHOD(Get) ( 
        /* [in] */ LPSTR szExpr, 
        /* [in] */ IGXObject* pData, 
        /* [in] */ IGXObject* pMark, 
        /* [out] */ IGXBuffer** ppBuff 
    ); 

    // IISNTemplateMap Interface 
    STDMETHOD(Put) ( 
        /* [in] */ LPSTR theKey, 
        /* [in] */ LPSTR theValue 
    ); 

private: 
    // Data structure for storing our data 
    ISNMapDictionary* theDictionary; 

    // Required for reference-counting macros 
    GXBasic m_GXBasic; 

    // Used to make sure object is thread-safe (Get() and Put() functions are atomic) 
    GXCRIT_SECTION m_Crit; 
};  

Our implementation class, CISNTemplateMap, inherits from our interface class, IISNTemplateMap. It also includes declarations for all of the pure virtual functions we must implement -- from IUnknown down to IGXTemplateMap to our own IISNTemplateMap.

The QueryInterface() Code

In the implementation code, things get a bit more interesting. The QueryInterface() code looks like Example 11.


Example 11
STDMETHODIMP
CISNTemplateMap::QueryInterface(REFIID riid, LPVOID *ppvObject)
{
   if (!ppvObject) {
       return E_INVALIDARG;
   }

   *ppvObject = NULL;

   if (GXGUID_EQUAL(riid, IID_IUnknown)       ||
       GXGUID_EQUAL(riid, IID_IGXObject)      ||
       GXGUID_EQUAL(riid, IID_IGXTemplateMap) ||
       GXGUID_EQUAL(riid, IID_IISNTemplateMap))
   {
       *ppvObject = (LPVOID) this;
   }

   if (*ppvObject) {
       ((IUnknown *) *ppvObject)->AddRef();
       return NOERROR;
   }

   return E_NOINTERFACE;
}

The caller supplies a REFIID  (which is really just a constant reference to an IID -- that is, const IID&) and a void pointer to a pointer (a handle), ppvObject. QueryInterface() does some simple error checking to make sure that the pointer isn't pointing to a NULL pointer. The first if statement tests to see whether this object can support the interface being queried. This is done by comparing the constant IIDs for the various interfaces supported by this object with the IID being supplied in the function call. For our class, CISNTemplateMap, we support IUnknown, IGXObject, IGXTemplateMap, and IISNTemplateMap. If any one of the GXGUID_EQUAL(...) macros returns a successful match for the comparison, we assign our object (via the this pointer) to the handle by doing a hard cast to make it a void*.

Our final check is to see whether our assignment worked. If it did, we got an interface match and we can call AddRef() on the object we're returning through the handle. Otherwise, we return an error, E_NOINTERFACE, so that the caller knows the object does not support the interface we queried (as demonstrated in Example 12).


Example 12
// Get an ISNTemplateMap.
IISNTemplateMap* pISNMap = ISNCreateTemplateMap();

// Test to see whether the object being pointed to by our IISNTemplateMap pointer 
// (pISNMap) supports the IGXTemplateMap interface. 
IGXTemplateMap* pGXMap = NULL; 
HRESULT hr = GXE_SUCCESS; 

if (((hr = pISNMap->QueryInterface(IID_IGXTemplateMap, (LPVOID*) &pGXMap)) 
    != E_NOINTERFACE) && (pGXMap != NULL)) 
{ 
    cout << "Interface supported." << endl; 
    pGXMap->Release(); 
} else { 
    cout << "Interface NOT supported." << endl; 
}  

Here we use QueryInterface() to see whether the object being pointed to by the IISNTemplateMap pointer supports the IGXTemplateMap interface. But notice how this same call to QueryInterface() also binds the object to a new interface pointer -- in this case, IGXTemplateMap. Thus we can use QueryInterface() in a multiple-inheritance scheme to dynamically cast our object to use two different interfaces, depending on the need. In fact, this is exactly what NAS does with the object being returned from an Execute() call through an IGXHierQuery interface. The caller gets back some object bound to an IGXHierResultSet interface pointer, but can turn around and call QueryInterface() on that object and "magically" transform the IGXResultSet into an IGXTemplateData! Example 13 illustrates this.


Example 13
IGXDataConn* pDataConn = NULL;

// ...Get the database connection...

// Get a hierarchical query object. 
IGXHierQuery* pHq = NULL; 
CreateHierQuery(&pHq); // Bind the interface to its implementation. 

// Load our flat file query into our query object. 
IGXQuery* pQuery = NULL; 
LoadQuery("myQueryFile.gxq", "myQuery", 0, NULL, &pQuery); 

// Add the query to our hierarchical one. 
pHq->AddQuery(pQuery, pDataConn, "myHierQuery", NULL, NULL); 

// Execute the query. 
IGXHierResultSet* pHierRS = NULL; 
pHq->Execute(0, 0, NULL, &pHierRS); 

// Magically transform our HierResultSet into a TemplateData. 
IGXTemplateData* pTData = NULL; 
pHierRS->QueryInterface(IID_IGXTemplateData, (LPVOID*) &pTData); 

// Voila! A TemplateData. Now we can call EvalTemplate(). 
EvalTemplate("template.html", pTData, NULL, NULL, NULL); 

// Release our resources. 
pHq->Release(); 
pQuery->Release(); 
pHierRS->Release(); 
pTData->Release();  

Note that at the end of this code, we still need to release both IGXTemplateData* and IGXHierRS* because we have two references to the same object. The QueryInterface() call, if successful, will return the object passed to AddRef().

The AddRef() and Release() Code

Now that you've seen how the more complicated QueryInterface() works, let's look briefly at the other two functions from IUnknown, AddRef() and Release(). These functions perform simple operations -- one increments a counter and the other decrements it. When the counter reaches 0, the object is destroyed by deleting the this pointer; this in turn causes the object's destructor to be called. The implementation of these functions is shown in Example 14.


Example 14
CISNTemplateMap::AddRef()
{
   GXUTIL_ADDREF();
}

CISNTemplateMap::Release() 
{ 
    GXUTIL_RELEASE(); 
}  

This is rather simple from a development standpoint because Netscape has provided us with macros that perform the reference-counting details. If we dig beneath the surface, we see that these macros are defined as shown in Example 15.


Example 15
#define GXUTIL_ADDREF()                                         \
   if (1)                                                       \
      return m_GXBasic.IncrRefCount();                          \
   else 0

#define GXUTIL_RELEASE()                                        \
   if (1)                                                       \
   {                                                            \
       ULONG count;                                             \
       GXASSERT((m_GXBasic.GetRefCount())> 0, GXASSERT_ERROR,   \
                "TOO MANY RELEASES");                           \
       count = m_GXBasic.DecrRefCount();                        \
       if (!count)                                              \
           delete this;                                         \
       return (count);                                          \
   } else 0

Both macros use the same basic object, m_GXBasic. This is an object of the class GXBasic, which actually contains the reference count for the object (along with a GXSYNCVAR to keep AddRef() and Release() atomic). Thus, in order for the macros to work, all your COM objects must contain the GXBasic class, and the object being reference-counted must be named m_GXBasic.

AddRef() is straightforward; it's a light wrapper around the GXBasic member function IncrRefCount(), which returns the current reference count after the increment. Release() is a bit more complicated, as it includes an assertion macro to make sure the current reference count is greater than 0 before we reduce it. (Actually, in the production release, GXASSERT(...) is a no-op, so this check is made only during a debug build of NAS.) After the assertion macro, we decrement the reference count and get the new value after the decrement. If the count variable has dropped to 0, we call delete this and return the count.

As you can see from our CISNTemplateMap class, there are several steps we've had to complete in order to develop a utility class within NAS that supports the COM model. But the results are worth it. With the example above, we've written our own equivalent to the NAS GXTemplateMapBasic class and can now add whatever functionality we want to the template map. We can overload Put() and Get() with our IISNTemplateMap interface to include other data types, such as long and int. This isn't possible with GXTemplateMapBasic, but only with our CISNTemplateMap, snugly hidden behind an interface. And since we've subclassed our interface from the NAS IGXTemplateMap interface, we can use a CISNTemplateMap wherever we would have used GXTemplateMapBasic (as in EvalTemplate() calls, for example).

TO SUMMARIZE

Netscape Application Server provides the framework for developing large-scale -- and, if you're not careful, potentially complicated -- software entities. Breaking down the business logic into two layers, an application layer and a service object layer, is the first step toward creating a manageable, extensible architecture.

The AppLogics, which reside in the application layer, should be derived from specialized base classes from the GXAppLogic class. These specialized base classes are created according to their functionality. A single derived base class (like our CISNAppLogic class) from which all specialized classes derive is handy for localizing functionality that's likely to be needed by all AppLogics.

The software entities in the service object layer should serve some simple utilitarian purpose (as do template maps, for example), or provide either a "service" of some kind (such as supplying run-time configurable information) or access to the database (for example, via objects that wrap database tables).

As we saw in our example of a template map COM object, we need a specific IID to associate with our interface (written in an IDL file), as well as accompanying implementation header and code files. Since all COM objects in NAS derive from IGXObject (and ultimately from IUnknown), we must support the QueryInterface(), AddRef(), and Release() functions. Having done this, we end up with a flexible and robust utility class that can be used by other service objects or applications.


FURTHER RESOURCES


View Source wants your feedback!
Write to us and let us know
what you think of this article.

Many thanks to Robert Husted and Mike Lee for their technical review of this article, and, of course, to View Source editor Paul Dreyfus.

Steve Malmskog is Director of Product Development for the Internet Shopping Network. The Internet Shopping Network is home to First Auction -- the world's only real-time online auction, featuring consumer electronics, computers, home and leisure merchandise, and jewelry. When he's not busy designing applications or writing code, Steve enjoys playing tennis and basketball and lending a helping hand at his church.

(8:98)


Related Readings:


Any sample code included above is provided for your use on an "AS IS" basis, under the Netscape License Agreement - Terms of Use

spacer spacer


                                                       
iPlanet International | Year 2000 | Site Map | Feedback
Products | Solutions | Support | Services | Download | About Us | Developer
© 2000 Sun-Netscape Alliance. All Rights Reserved  Privacy Policy