Object Service Architecture

Intermediary Architecture Infrastructure

Project Summary

Paul Pazandak
Object Services and Consulting, Inc.
September 15, 1998

Executive Summary

The Intermediary Architecture Infrastructure is an enabling technology which facilitates the dynamic augmentation of applications with added functionality. Specifically, it enables the run-time insertion of object services to extend the behavior of applications on the fly. The implementation of the infrastructure is application-independent and minimally requires the addition of only three classes to be integrated with any application. Further, the infrastructure is itself component-based (and very extensible), and can self-assemble on-the-fly as required. We have used this infrastructure to extend the functionality of a web server , a proxy server, and Netscape's Mozilla Web Browser. In these implementations we've added services to dynamically insert some of the following functionality: authorization, change notification, web page content modification, network weather service, URL request modification, and assisting to provide Mozilla with a dynamic user interface.


The world-wide web (WWW) is composed of web clients and web servers and an underlying communications infrastructure capable of supporting any number of high level protocols such as HTTP, IIOP, and RMI. Given the variety of uses (and anticipated uses) of the WWW, clients and servers need to be flexible enough to satisfy the wide range of applications. As it is unlikely that they cannot do this out of the box, web clients and servers need to be able to solve this by being extensible. In fact, most web clients and servers do offer some degree of extensibility through the use of browser plug ins, server-side includes, DLLs, Java applets, Javascript, etc. to provide added functionality.

As an alternative we have explored the use of distributed object component architectures (e.g. CORBA, DCOM, etc.) as an avenue to provide extensibility to the WWW so we could better understand how such distributed architectures scale to the internet. This document presents an overview of the implementation of an infrastructure which facilitates this goal.


Addressing the goal above, the Intermediary Architecture project focused on designing an infrastructure to integrate web architectures and distributed object component/service architectures.

The principal objectives of this project included:


Given that our focus was to extend, and not to rebuild current web architectures, we needed to develop an architecture that would allow us to augment the current functionality of web applications. However, the closed nature of these systems was a limiting factor. Given this, conceptually what we wanted to do was to insert a layer between clients and servers (see Figure 1 below). This would provide an access point from which we could monitor web events, and conditionally augment the apparent behavior of the web client and web server by invoking object services.
Figure 1. Conceptual Intermediary Architecture

From an implementation standpoint this could be accomplished in one of two ways. First, we could implement the layer external to the application(s) of interest, and effectively filter the incoming and outgoing communications. This, for example, could be accomplished by using a web proxy through which a client and server would communicate. Alternatively, a second implementation approach would be to tie directly into an application, monitor internal communications, and selectively augment the behavior of that application.

The first approach is necessary when the applications are closed, since only the inter-communications can be intercepted and possibly modified. However, the approach is limited because it can only effect change through modification of inter-application communications. On the other hand, the second approach can be used when the application's design exposes suitable hooks from which this external architecture can augment the application's behavior by accessing and modifying intra-application communications (and data structures). If the appropriate hooks, or insertion points, needed to effect the desired changes are not available, it might be necessary (if at all possible) to modify the application's source code to expose such points.

We can effectively implement both approaches using the second architecture described above and illustrated in Figure 2 below. Using binding code, which accesses (some portion of) the application's internals via available hooks or source modification, interesting events are filtered and passed on to the Intermediary Architecture for processing and possible behavior augmentation via the execution of local and remote object services.

Figure 2. Implementation Architecture

Granularity of Augmentation
Once the insertion point has been identified (some applications could conceivably have a number of insertion points) and binding code implemented, the next issue to address is when to augment the behavior of the application. Say, for example, that the application is a web server and that we would like to insert an HTML-based menu on each page returned. Then, we would insert this service-provided functionality (which modifies a page before returning it to the client) every time an HTTP GET occurs. The granularity of augmentation is then set coarsely at the level of a GET event.

Alternatively, we may decide that we want to retrieve and insert annotations applicable to some subset of pages (internal scientific reports). While this behavior augmentation is still applicable to HTTP GET events, the granularity has become finer -- to that of a single page.

In fact, any level of granularity can be supported within the IA. What is required is that a service specification  exists which describes what services should be invoked for a given event (and target). In the first example, since all pages were to be modified when a GET event occurred, a specification need only identify the services and associate them with the event GET. In the second example, a specification would not only have to list the services, but also the targets (or in this example, specific web resources) to which they should be applied. Therefore, in general, a specification must include three things: an event, a (possibly null) target, and a set of services to execute.

Note that a target object can be anything: a single web page or file, a client's IP address or name, the time of day, etc. The target can be simple, or it can be complex (e.g. time and a file). The target could also be derived from a conditional statement. Given this event-target information pair, the IA will retrieve the appropriate specification if it exists, and execute the defined services. Since service execution is tied to the occurrence of events, we call this event-based behavior augmentation. The use of targets then enables us to control the granularity at which the augmentation occurs.

Service Specifications
We have mentioned the need for service specifications which define what services to execute. Further, we tie these services to an event-target pair. The format of a specification is not constrained as components of the IA can be specialized to handle any format. As such, there must be some means to determine the format of the specification so that the proper component specialization (called a Specification Manager) can be loaded to access it. For example, the format may be stored in a database, or embedded within the specification itself; it may be written in XML or IDL, etc.

Service Execution
Given that the specification format is unrestricted, it is also necessary to identify which specialization of IA component will direct the execution of the services defined within the specification (the class of component in charge of service execution is called the Dispatch Director) . Before addressing this in greater detail we need to discuss the kinds of behavior augmentation that might be supported.

First, what constitutes an event can be defined by the application or the binding code, there is no rigid definition. We could decide that whenever x=6 we will define that as an event. Or when any HTTP method invocation (GET, PUT, POST, etc.)  is received from a client could be defined as an event. Next, we need to identify when we can execute services to augment the behavior of the application in response to this event. It may be possible to capture the event and execute services before the application performs its normal action in response to the event; or, after the normal action; or, perhaps it may be possible to deactivate the application's normal action, and perform a replacement action. The LISP language has a concept similar to this called around methods. We call the temporal points (in relationship to the occurrence of an event) at which services can be inserted the phases of augmentation.

Again referring to a web server example involving a client-submitted GET event where the target is a file resource, a before action phase might invoke a service which alters the URL to redirect the request for server load-leveling; an after action phase service might insert an HTML-based menu or pertinent annotations into the file to be returned to the client; and, a replacement action phase service might dynamically generate the content of the page from a database.

So, depending upon the architecture of the application, its openness, the programming language used, etc., some or all of these phases may be supportable. Therefore, in addition to specifying the services for an event-target pair, we need to identify in which phase the services should be executed. So, the set of services to be executed is then associated with an event-target-phase triplet. A sample service specification in XML is here.


The interface of a service will depend upon which phases it supports -- there is a specific API for each invocation phase. In fact, generically, a service that implements the generic high-level API could conceivably be used in any implementation of the IA architecture. A simple example is a service that just counts the number of events of each type which occur.

When a service is invoked, at a high-level the application state is passed into the services via request and reply objects. For before-phase services, a request object is passed in, and returned (services can only modify the request); for replacement-phase services, a request object is passed in, and a reply object is returned (services must return a non-null reply object); and, for after-phase services, a reply is passed in, and a reply object is returned (services can only modify the reply). This high-level di-parametric approach frees the IA core architecture from implementation details and is one reason that the implementation is reusable.

However, to write a generally useful service, access to application state will be required. Therefore, once a service is passed in the request and/or reply objects, they are cast to context-specific object types so that more of their content is exposed. This context/API can be generic enough so that different applications of a given kind (e.g. all web servers) can share services (see interoperability issues for a more detailed discussion). However, a service could as well cast the objects to application-specific types and expose application-dependent structures. Yet it is important to understand that the context-specific types provide a means to hide or wrap implementation details so that services can interoperate between IA implementations.

Approach Summary

The purpose of the IA architecture is to enable application behavior to be augmented with local and remote object services. This is done by identifying application access points, and writing binding code that extracts application state (events and target information) and passes it on to the IA. The IA in turn loads in the appropriate service specifications (if they exist), executes those services, and passes the results back to the binding code. If necessary the binding code may merge those results back into the state of the application.

While the IA has a demonstrated ability to insert object service technology into WWW applications, it may actually be used to augment the behavior of many kinds of applications.


The implementation of the IA included in this distribution was the result of two successive versions. The first version focused on augmenting the resource-specific behavior of web servers, specifically the Jigsaw web server, with object service functionality. While it was successful at extending the functionality of the server, the implementation of the IA was tied to Jigsaw. Therefore, the architecture didn't scale and was not reusable. Because of this, two primary objectives for the second version included reuse and scalability. To meet these goals we felt that the resulting architecture had to be extremely extensible to be able to adapt to meet the needs of most any environment or application.

While the first version was dynamic, in that it could load and execute services at the granularity of an event-target pair, its underlying architecture was not extensible. That is, each of the components of the IA had specific capabilities that could not be changed to adapt to different environments or applications. This severely limited its ability to be reused. What we felt was required was an architecture that would allow component-wise substitution. This would allow new components to be designed and used at anytime without requiring a re-architecting of the IA.  This solved the problem of reuse. However, given the diversity of uses of the WWW and kinds of data being accessed, it was important that each implementation of the IA was not tied into a particular configuration thereby constraining what it could actually do. For this reason we made the IA dynamically reconfigurable at the level of an event-target pair. What this meant was that the components of the IA would be assembled on the fly, and more importantly, that different implementations of those components could be used on each invocation.

This approach allowed us to define an infrastructure for augmenting the functionality of any application without tying it to any specific (static) implementation. Furthermore, the components of the IA could themselves be object services retrieved and loaded at runtime from the WWW. Note, too, that reuse was also partially enabled by the use of the di-parametric approach as described above.

In the second version of the IA, the primary components and invocation process are shown in the following figure (Figure 3).

 Figure 3. IA Components and High-level Component Interaction

Primary Components

The above graphic (Figure 3) outlines the high-level interaction of components. It also illustrates at a high-level how this architecture interfaces to an application. The entire generalized process from event interception to service execution supported by this design will be discussed.

The Point Of Interception

At some point within an application (an intercept point), using some built-in architecture/framework support, language support, ad-hoc code instrumentation, or by intercepting communication pathways external to the application, the IA can be used to augment to application's normal functionality with additional behaviors. Depending upon the interception mechanism that is used, this normal functionality may take any form in the local application, such as a method, an application-defined event, or even a line of code. To operate in the context of the IA the application's normal actions must be mapped to some event identifier(s), minimally so that the services we want to insert at this intercept point can be identified, retrieved, and performed. The manner in which an event's identity is defined can be application-specific, but there must be some means given an event, to locate and return the appropriate services to perform.  How the event is detected and intercepted is an application-specific detail we do not address. It is also the application's responsibility to map its internal representation of an event into an event object understood by the architecture.

Note that in the graphic above, the even-numbered arrows are pointing to component object types, and perhaps not more appropriately to instances of these types. This is because we are mixing dynamic and type models in one illustration... hopefully this will not be confusing. In addition, this discussion pertains to an abstract implementation, and therefore refers to abstract IA classes when appropriate. Actual implementations, in most cases, may use the default implementations of these classes which are provided in the distribution.

Event Objects (arrows 1/2)
The first step, once (the intercept mechanism's interpretation of) an "event" occurs at the intercept point, is to create an Event Object. An event object is an architecture-consumable representation of some application functionality we want to augment. To create an event object, the binding code must invoke a creation method on the Event Object Factory. This invocation, with supplied parameters, causes the Event Object Factory to return an appropriate (new) instance of an event object subtype to the intercept mechanism. A Basic Event Object simply contains an event identifier, although the structure and behavior could also be much more complex by specializing the Event Object.

The extensibility offered by implementing specialized Event Objects is the ability to more accurately model (and handle) application-specific representations of events. The data within these events can then be consumed by other specialized IA components, as well as the services invoked in response to the event.

* The individual object factories depicted above represent a logical implementation. The actual implementation of the factories, is as a single class: com.objs.ia.factory.iaObjectFactory . As with the other IA components, the iaObjectFactory may also be extended.

Interceptor Objects (3/4)
Once an event object has been instantiated the intercept mechanism (which we also call the binding code) requests an Interceptor Object (com.objs.ia.interceptor.InterceptorObject) from its factory. From this point on, the intercept mechanism interfaces with the Interceptor object, while the Interceptor Object interfaces to the Metadata Accessors and Dispatch Directors of the IA. The Interceptor Objects determine how the component interaction takes place, e.g. what metadata is requested from the Metadata Accessor, and when and how the functionality of the Dispatch Directors are invoked. While there will be default objects for each of the components described, new object subtypes can be created (new factories could be created as well, along with services to locate factories, but we do not address this possibility).

Once a new Interceptor Object is returned to the intercept mechanism, the intercept mechanism needs to ask the Interceptor Object to process the Event Object. How this actually occurs depends upon the interceptor (and actually on the mechanisms available to intercept the event). Using the basic Interceptor Object (com.objs.ia.interceptor.BRAInterceptorObject, our default implementation),  it would be possible to execute services before the event, after the event, and in lieu of or instead of the event.

The demonstration code we have included in this distribution uses this class. In one demo, the binding code that instantiates and interacts with the Interceptor Object is com.objs.ia.JigsawServerInterceptPt.java. It binds the IA to the Jigsaw Web Server. This class, and the specializations of the Request and Reply objects (com.objs.ia.request.JigsawRequest and com.objs.ia.reply.JigsawReply)  are all that is necessary to create, and the only classes which are Jigsaw-specific.

The extensibility offered by implementing interceptor object subtypes provides the ability to control how event augmentation proceeds. It must understand how to interpret and process event objects (for the event object subtypes it accepts). It however does not need to understand the format of the metadata, or service specifications.

MetadataAccessor Objects (5/6)
Once the Interceptor Object has been invoked it requests a new Metadata Accessor Object (com.objs.ia.accessor.MetadataAccessor) from the corresponding Factory so that it can retrieve the appropriate service specifications. The Metadata Accessor Object returned must be aware of where the specifications are stored, or how to look for them (for the given object). It does NOT need to understand the format the specifications are in. This requires that the Interceptor Object knows what to ask for from the Metadata Accessor factory, as well as how to configure the new Metadata Accessor object. The interceptor binding code defines the type of Metadata Accessor to instantiate based upon its knowledge of where the service specifications are stored. For example, if the specifications are stored in local files (e.g. perhaps where there is a specification file per web resource), then the FileMetadataAccessor (com.objs.ia.accessor.FileMetadataAccessor) would be used. If the specifications were accessible via URLs, then the URLMetadataAccessor would be used (com.objs.ia.accessor.URLMetadataAccessor). Other specializations of MetadataAccessor could be implemented as well.

Once the new Metadata Accessor has been returned from the factory and configured through initialization, the Interceptor Object can request a Service Specification Manager which is responsible for retrieving the specific service specifications for the event to be augmented. The Metadata Accessor returns an appropriate Service Specification Manager Object capable of understanding the structure and language of the service specification.

The extensibility offered by implementing Metadata Accessors includes flexibility in defining how the metadata is stored and retrieved (e.g. separate files, a DBMS, etc.).

Specification Objects (7/8) (now called Specification Manager Objects)
As described above, upon request the Metadata Accessor returns a service Specification Manager Object (com.objs.ia.specification.SpecificationManager) to the interceptor, which it in turn uses to retrieve the specifications and then passes them off to a Dispatch Director to execute. The service specifications are grouped by phase, so that the Dispatch Director will ask for the before, replacement, or  after set of service specifications to execute. The Specification Manager Object  reads in the service specifications and instantiates Specification Objects (aka Generic Service Objects) -- one per service, which are object representations of the services described in a specification. The format of the Specification Objects returned by the Specification Manager  is determined by the metadata specification itself -- either the Metadata Accessor determines the Specification Object type from what it knows or can derive from the specification, or an attribute value in the specification itself indicates what kind of Specification Object should be used (one kind of Specification Object is used for all services contained within a single specification). In both cases, the Metadata Accessor requests the appropriate Specification Manager Object from its factory which understands how to convert service specifications into IA service Specification Objects (instantiated as com.objs.ia.specification.GenericServiceObject objects).

Consider Specification Objects as high-level service wrappers. They will wrap services of any type. For instance in this distribution, the GenericServiceObject type is used to wrap services whether they are Java, CORBA, or DCOM-based. This provides a layer of insulation between the instantiation and execution of services. Service execution is handled by the Service Objects.

The extensibility offered by implementing Service Specification Manager Object subtypes is that it enables more complex service Specification Objects to be defined (the basic specifications are structured as a simple list of services to be executed). For example, extending this object type would be one step that is required if ECA-related condition statements or scripts were to be embedded within specifications. In addition, it enables multiple specification formats to be supported (e.g. HTML, IDL, XML, etc.), what specification and service object subtypes will be used to represent these specifications as services, and what other kinds of metadata it will return.

The definition of a Specification Manager Object type directly affects the capabilities of the Dispatch Director to interpret and execute specifications.

DispatchDirector Objects (9/10)
Once the Interceptor Object has initialized the Specification Manager Object, it must pass it to a DispatchDirector (com.objs.ia.dispatch.DispatchDirector) for processing. How the specification is executed is controlled by the Dispatch Director, so the Interceptor Object must request an appropriate Dispatch Director from its factory (the DispatchDirector Factory). Each set of specifications for a target will specify what kind of Dispatch Director is required to execute the specification -- the Interceptor Object uses this information when requesting a new Dispatch Director from its factory. Once the new Dispatch Director is invoked, it retrieves the appropriate portion (by phase) of the specification from the Specification Manager, and executes the services contained within that set of specification objects. This execution process is guided by the design of the director, so variations are possible. The basic or default behavior is to execute the default service specification object (simply a list of the services) sequentially (see com.objs.ia.dispatch.BRADispatchDirector).

The extensibility offered by implementing DispatchDirectors is primarily over how specifications (and the services within) are executed. While the default behavior was described above, significant enhancements could be added (as with the other components). For example, the default behavior of the IA could be interpreted as implementing the event-condition-action (ECA) profiles of EC-x-A, or E-x-CA. Using the three potential places for any of these to occur (application-dispatcher-service),  the EC-x-A profile corresponds to event detection (E) and condition checking (C) occurring in the application, nothing occurring in the dispatcher, while action execution (A) would occur in the service. An EC-x-A profile applies when it is solely the application's responsibility to determine when a service is executed. However, in general, it will be up to the service itself (which corresponds to an E-x-CA profile). For the E-x-CA profile, E occurs in the application, nothing in the dispatcher, and CA occurs in the service (so the service performs condition checks to see if it should perform the associated action). However, by extending the specification language to support conditional statements, the profile could become E-C-A; this moves condition checking (C) to the dispatcher, which is an optimization, as the service is not executed unless the conditions are met. In reality, condition checking of some sort is occurring in both the application and the service (EC-x-CA). By enabling the dispacher to perform some types of condition checking to reduce service invocations when they are unnecessary we gain a better optimized execution (the resulting profile would be EC-C-CA). However, more importantly, other event models could also be supported via this extensibility.

In general, there must be some agreement between the binding code, Interceptor Objects, and Dispatch Directors regarding how an event is processed and specification is executed. Thus, specification structure is not independent of the Dispatch Director used.

Service Objects (11/12)
The Service Specification Manager Object passed to the Dispatch Director provides access to the services to invoke them. By default each Specification Object (instantiated as a com.objs.ia.specification.GenericServiceObject, or a specialization of this), contained within the specification will indicate its implementation type. Using this information, the Dispatch Director will request a type-specific Service Object (com.objs.ia.services.ServiceObject) from the Service Object Factory. This new instance of a Service Object subtype will understand how to execute the service described by the instance of Generic Service Object. This enables services of any kind to be defined and invoked, whether local or remote, since the actual implementation of the service is encapsulated by the service object (which can be actually viewed as a service wrapper). We could, for example, implement services in Java, in C++, in LISP; or use distributed programming architectures such as CORBA, DCOM, or ActiveX. The version of the IA in this distribution supports local and remote services written in both Java and CORBA. (The implementation type in the default case would be either "Java" or "CORBA".)

While the example below shows what a sample service specification looks like, the format of a specification can be significantly more complex. This is controlled by the Specification Manager Objects (which  read them), the Service Specification Objects (which contain them), the DispatchDirectors (which must understand how to execute them), and the Service Objects (which must understand how to execute each service specified in the language).
The extensibility offered by implementing Service Object subtypes is specifically the ability to incorporate and use services implemented in any way possible since their implementations can be encapsulated within these Service Objects. This, in turn, enables dynamic extensibility of the environment this architecture has been tied into because the environment need not be aware of the implementation of the services prior to their execution.

Architectural and Implementation Issues

Specification-level Interoperability
Service-level Interoperability
Figure 4. Parameter Wrapping and Passing

Sample Service Specification

In this specification example, the application being augmented is a web server. The target is the server (so the same specification is used for all events). The specification is divided into service sets, where each set describes an event-phase pair. The first XML element IASpecification includes an attribute dispatchertype, which identifies the type of Dispatch Director that must be used to execute this specification (in this case, BRA, which is a dispatcher which knows how to execute specifications that use before, replacement and after augmentation phases). The value BRA is passed into the object factory, and the correct dispatcher is returned.

This particular format of specification written in XML is understood by the XML Specification Manager (com.objs.ia.specification.xml.XMLSpecificationManager). Each specification includes a set of parameters that are required to locate and invoke the service. The Params element includes a set of service-specific configuration parameters which are passed directly to the service upon invocation.

 <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE iaspec PUBLIC "-//OBJS//DTD IASpecification//EN" "IASpec.dtd">

<IASpecification dispatchertype="BRA">
    <ServiceSet phase="BEFORE" event="GET">
            servicename="Internet Weather Service"
            serviceid  ="com.objs.ia.Weather.weather"
            <Params mdr="http://www.objs.com/weather/MDR:8017"
            servicename="Basic Public-Private Routing Service"
            serviceid  ="com.objs.ia.router"
            <Params public="/Public" private="/Conf" privFilter="*.objs.com"
                     pubFilter="*.edu|*.gov" other="/Sorry.html"
    <ServiceSet phase="AFTER" event="GET">
            servicename="Internet Weather Service"
            serviceid  ="com.objs.ia.Weather.weather"
            <Params mdr="http://www.objs.com/weather/MDR:8017"

Additional Notes:

Architecture Implementation
The architecture we have implemented to integrate the WWW with distributed component architectures is not itself built upon distributed services, but it could be. Each of the components could in fact be located remotely. However, while the services which may be invoked by the IA may be written using CORBA or DCOM for example, the components of the IA are pure Java. This restricts the implementation of these components as they do not reap the benefits of either distributed architecture. Therefore, one improvement might be to migrate the architecture by adopting either (or both) CORBA or DCOM as the underlying distributed architecture for the IA.

Implementation Optimization
Service specifications are used to dynamically assemble services together to augment the behavior of an application. However, the specifications are also used to dynamically assemble the components of the IA as well (part of the metadata stored in the specifications aids in assembling the IA). But, if the components will always come together in the same fashion then an optimization would be to keep those components assembled and waiting for the next invocation. This optimization has been at least partially implemented.

Demonstrated Applications of the Intermediary Architecture

To demonstrate the applicability and viability of this architecture we designed and coded three implementations:
  1. The first implementation augmented the Jigsaw server's functionality at the resource level. We made up numerous services which could be invoked based upon a resource specification (each resource could have its own specification). These services provided dynamically alterable authorization, compression/decompression, and change notification. Further, a specification could be easily modified at runtime.
  2. The second implementation augmented Jigsaw's functionality as a proxy. While the first implementation was useful for resource-specific behavior augmentation, we could also use Jigsaw as a proxy and augment its functionality. At this level we could define one specification which would alter the proxy behavior for every access. We implemented a shell service and a network weather service. The user-centric network weather service (available in a separate distribution) recorded the response times for each and every proxy request and stored them in a database.
  3. The final implementation was intended to test the broad applicability / generic design of the architecture. In this demonstration we modified the Netscape Mozilla browser. First, we intercepted URL requests in the browser (in C++) and sent them off to the IA (via a DLL which used JNI to call Java). The IA, via a specification, simply redirected specific URLs to other sites. It could as well have checked the user's authorization for those pages. We then implemented a second intercept point which allowed us to modify the content of the returned pages (as dictated in a specification). Finally, we modified Mozilla to support a dynamic user interface. Then, based upon the pages loaded we could add new toolbars and buttons to the associated window. While much of this demonstration required a significant amount of time to implement, more than 95% of it was in Mozilla -- no modifications to the IA were required, only additions to handle the specific augmentation services.  Given the rapid evolution of the free-source Mozilla code, we have not included our code since it was tied to earlier releases of the Mozilla code.

Lessons Learned

Next Steps

Some primary next steps to focus on include: Some longer term issues include:

This research is sponsored by the Defense Advanced Research Projects Agency and managed by the U.S. Army Research Laboratory under contract DAAL01-95-C-0112. The views and conclusions contained in this document are those of the authors and should not be interpreted as necessarily representing the official policies, either expressed or implied of the Defense Advanced Research Projects Agency, U.S. Army Research Laboratory, or the United States Government.

© Copyright 1997, 1998 Object Services and Consulting, Inc. Permission is granted to copy this document provided this copyright statement is retained in all copies. Disclaimer: OBJS does not warrant the accuracy or completeness of the information in this survey.
Last revised: September 15, 1998.  Send comments to Paul Pazandak.