fbpx

A Practical Guide to the Business Object Framework

Introduction

The
Business Object Framework
(BOF) is a set of functionality
included starting with DFC 5.1 that provides ability to hook into any of the methods in the
standard DFC object interfaces. This enables a developer to customize business logic
behavior at the DFC level, such that any application which leverages DFC will automatically
use the BOF extensions you provide.

For an overview of what BOF is and the value it can provide, you should probably start
with Michael Trafton’s article Introduction to the Business Object Framework, which
provides some high level understanding of what BOF is used for and how it is used, as well
as a discussion of some real-world examples.

This article provides a practical guide for implementing BOF extensions and and
deploying into your Documentum applications. Once you’ve read this article, you can expect
to have an understanding of how to implement simple
type based
objects

(TBOs) and
service-based objects
(SBOs), how to
package them and deploy them into both pre- and post-5.3 environments, and some of the
common pitfalls and challenges you might face along the way.

When BOF code is called (and when it isn’t)

Any application which uses DFC to interact with the repository will automatically use
BOF, assuming your TBOs/SBOs are registered in that environment/repository.

Code which does not use DFC to interact with the docbase will not be able to leverage
your BOF customizations. The big thing to keep in mind here is docbasic code
which runs either as part of a Method configured in the docbase or as part of the
lifecycle scripts. Since dmbasic talks directly to the DMCL API, it does not go through
DFC and therefore does not get the benefit of the BOF layer.

BOF Development

Documentum provides a number of recommended guidelines for BOF development. Although somewhat
out-of-date, the 2003 white paper “BOF Development Guidelines” is an excellent resource.
At a high-level, you should always take care when writing BOF code. It can easily be a
source of bugs which are tricky to track down. But the basics of BOF development, and most
of the simpler tasks for which one might use BOF, are fairly straightforward.

At a high level, here are some of the author’s recommendations for BOF development,
gleaned from Documentum guidelines and experience with BOF:

  • Code to interfaces as much as possible, and avoid casting to concrete classes.
    Many of the DFC classes for dealing with docbase objects are proxied implementations
    rather than concrete Documentum classes.
  • Leverage SBOs to abstract common functionality. TBO development is
    very easy, and deployment of TBOs is a bit more straightforward than SBO deployment,
    so it can be tempting to just encapsulate all your functionality in a TBO class
    with no/few external dependencies. This works for simple TBOs, but once you start
    doing anything complex, or more importantly anything that can be reused by other
    TBOs, you may want to consider putting that functionality into an SBO that can
    act as a service to multiple TBOs.

Overriding Methods in TBOs

The main value of a type based object is the ability it gives a developer to
override selected functionality of DfSysObject, the base class for most objects
you’re likely to work with in a docbase. With just a small about of TBO code, a
developer can significantly alter the behavior of the object. This is done by
overriding the various DfSysObject methods.

Before DFC 5.3, the names and signatures of the DfSysObject methods that
needed to be overridden was inconsistent and unintuitive. Often it was not clear
which methods needed to be overridden in order to catch every possible case. DFC 5.3
introduced a new set of methods designed to make TBO development easier, by providing
clear extension points that require little understanding of the inner workings of DFC.
Before 5.3, if you wanted a piece of code to execute every time a file was checked out,
you had to override the checkoutEx method and the getFileEx2 method. This wasn’t
intuitively obvious, so many developers likely overlooked it. In 5.3, you can simply
override the new doCheckout method with the confidence that your code will be executed
every time a document is checked out.

Chapter 19 of the Documentum 5.3 System Migration Guide provides more detailed information
about the specific method names and the situations each is for.

If you know your BOF code will only be used in a DFC 5.3 environment, you are strongly
encouraged to override these new “do” methods.

BOF Deployment

Deploying your BOF code can be a tricky enterprise. It is especially challenging in a
pre-5.3 environment, since the BOF code must be registered in DBOR on every single client
machine. This section provides an overview of what you need to do to deploy your BOF code
in both pre- and post-5.3 environments.

Before 5.3: The Documentum Business Object Registry (DBOR)

In a pre-5.3 environment, there are two steps to deploying your BOF
code:

  • Register your BOF code in DBOR.
  • Deploy your BOF code to the client application

We will begin with an overview of DBOR, then we’ll get into the specifics of
registering your BOF code in DBOR, and finally we’ll provide instructions for actually
deploying the BOF code into your client application(s).

Overview of DBOR

Until 5.3, the only way to use BOF customizations was to register them in the
Documentum Business Object Registry on every client machine which needed that BOF
functionality. This could include the WDK application server machine, the Java Method
Server, the Webcache Source, and other client machines. The word “client” here is meant
to imply an application that is talking to the content server over DFC, not necessarily
the end-user client machine. (In the case of Desktop Client, the end-user machine would
be the client, but for a WDK application, the app server is actually the DFC
client.)

This method of deployment, which is still supported (though not preferred) in 5.3,
requires that TBOs and SBOs be registered in the DBOR, which is essentially a text file
(dbor.properties) located under the DFC installation on that machine. TBOs and SBOs can
be registered directly by editing the text file, or through a DFC API
(preferred).

In addition to registering the TBO and SBO classes in DBOR, it is also necessary to
include the jar or jars containing the TBO/SBO code in the classpath of any JVM which
might call out to DFC on that machine. In a pre-5.3 environment, DBOR is a global
registry for the entire machine, so there is no way to deploy your BOF code to one client
on that machine without deploying it to all clients on that machine.

The 5.3 code line resolved this problem by abandoning the DBOR in favor of hosting
BOF code directly in the docbase. More on this later.

For a typical WDK application (such as Webtop or Web Publisher), you will need to
register your BOF classes in the DBOR registry on any machine which you want to make
use of your BOF code. In most cases, this is the application server machine that is
running WDK, but it could also include the Java Method Server (which typically runs on
the content server machine). For Web Publisher applications, BOF might also need to be
deployed into the Webcache (SCS) Source, which talks to the docbase through DFC (and
therefore through BOF) when publishing website files from the docbase out to a Webcache
(SCS) Target.

Registering in DBOR manually

The DBOR in DFC 5.2.5 is a properties file, a simple text file called
dbor.properties that lives in your DFC config directory. This file is read
by DFC to construct a lookup table for BOF objects, so that any DFC code that is
running on that machine knows how and when to instantiate BOF classes.

The structure of the file is very simple. Each line in the file must be one of the
following:

  • blank (ignored)
  • a comment, denoted by a leading pound (#) symbol (ignored)
  • a TBO configuration instruction
  • an SBO configuration instruction

The format for a TBO configuration instruction is as follows:

[object_type]=type,[TBO class name],[version]

In an actual DBOR configuration instruction, [object_type] would be
replaced with an object type name for an object type in the docbase. The [TBO
class name]
would be replaced with the fully-qualified Java class name
(including the full package structure) of the TBO class to use for that object type.
The [version] would be replaced with the specific version of that TBO
class to use. The version number specified here will be passed into the TBO
constructor; it is up to the TBO code itself to do something useful (or not) with this
version number.

The following line registers the MenuItemContentTbo TBO class against
the bf_web_content object type in DBOR:

bf_web_content=type,com.bluefishgroup.navigation.bof.MenuItemContentTbo,1.0

With this line in my dbor.properties file, any time I use DFC to load
a bf_web_content object from the docbase, the object I get back will be an
instance of MenuItemContentTbo. Anytime DFC code is used to manipulate
this object, it will use an instance of MenuItemContentTbo, which
guarantees that the custom TBO code in MenuItemContentTbo will run.

The format for an SBO configuration instruction is as follows:

[SBO interface name]=service,[SBO class name],[version]

In an actual DBOR configuration instruction, [SBO interface name]
would be replaced with a fully-qualified Java interface name (including the full
package structure) an object type name for your SBO’s interface. The [SBO class
name]
would be replaced with the fully-qualified Java class name (including the
full package structure) of the SBO instance class that should be loaded when client
code requests an SBO with that interface. And, like the TBO configuration, the
[version] would be replaced with the specific version of that SBO class to
use. Again, the version number specified here will be passed into the SBO constructor;
it is up to the SBO code itself to do something useful (or not) with this version
number.

The following line registers the MenuItemService SBO class against the
IMenuItemService SBO interface in DBOR:

com.bluefishgroup.navigation.bof.IMenuItemService=service,com.bluefishgroup.navigation.bof.MenuItemService,1.0

With this line in my dbor.properties file, any time I use DFC to load
a service for the IMenuItemService interface, the object I get back will
be an instance of MenuItemService. This makes it extremely straightforward
for someone to come behind me and implement a custom version of
MenuItemService. All they have to do is register their
MenuItemService SBO class against the IMenuItemService
interface, and any DFC code which uses the IMenuItemService will be
guaranteed to use their SBO code (assuming the client code is using the SBO model
correctly and coding to the interface, not the instance class).

Registering in DBOR using DFC API

The dbor.properties file is simple, which makes it easy to edit and
understand, but editing this file manually is not the preferred way of managing DBOR
registrations. Documentum provides a Java API for managing DBOR registrations, and it
is the recommended way of working with DBOR.

The following example illustrates how to register the
MenuItemContentTbo TBO class against the bf_web_content object
type:

// Set up some variables.
String tboObjectName = “bf_web_content”;
String tboClassName = “com.bluefishgroup.navigation.bof.MenuItemContentTbo”;
String tboVersion = “1.0”;

// Get a handle on an IDfDbor.
DfClientX clientx = new DfClientX();
IDfClient client = clientx.getLocalClient();
IDfDbor dbor = client.getDbor();

// Create a new DBOR entry.
DfDborEntry entry = new DfDborEntry();
entry.setName(tboObjectName);
entry.setJavaClass(tboClassName);
entry.setVersion(tboVersion);
entry.setServiceBased(false);

// Register the entry.
dbor.register(entry);

When run, this code will actually create the necessary configuration instruction
line in the dbor.properties file on the local machine.

Similarly, the code to register the MenuItemService SBO class is
included below:

// Set up some variables.
String sboInterfaceName = “com.bluefishgroup.navigation.bof.IMenuItemService”;
String sboClassName = “com.bluefishgroup.navigation.bof.MenuItemService”;
String sboVersion = “1.0”;

// Get a handle on an IDfDbor.
DfClientX clientx = new DfClientX();
IDfClient client = clientx.getLocalClient();
IDfDbor dbor = client.getDbor();

// Create a new DBOR entry.
DfDborEntry entry = new DfDborEntry();
entry.setName(sboInterfaceName);
entry.setJavaClass(sboClassName);
entry.setVersion(sboVersion);
entry.setServiceBased(true);

// Register the entry.
dbor.register(entry);

The author has also found it useful to query the DBOR before registering anything, to
be sure some other client has not already registered a TBO or SBO for the specified
object type or interface name. The method IDfDbor.isRegistered can be used
to perform this query.

Packaging the BOF code

The instructions above describe how to register your BOF code in DBOR. This is
necessary in order for Documentum/DFC to know which TBO/SBO classes to use when a
request is made for a TBO or SBO instance. But ultimately, this code cannot be used by
DFC untless it knows where to find it.

Documentum’s BOF guidelines suggest that BOF code be packaged in one or two jar
files: One jar file should contain the interfaces, and the other should contain the
implementation classes. Most TBO classes do not require corresponding interface classes,
since usually the purpose of a TBO is to override existing functionality in the IDfTypedObject
interface. Therefore, simple TBO deployment should only include one jar which contains
the implementation classes. An SBO must always have both an interface and an implementation
class, which means you would need to have an interface jar and an implementation jar; in practice,
you may find yourself coding an SBO implementation to an existing (perhaps already-deployed)
SBO interface, in which case you would only have the implementation class.

Deploying the BOF code

BOF classes need to be loaded by the same classloader which loads DFC. For this
reason, BOF classes can not be installed as part of a web application (i.e. in
WEB-INF/lib or WEB-INF/classes), as web applications are
loaded by a classloader specific to that web application. Placing your BOF code
inside the WEB-INF/classes folder of your webapp will not work. Instead, the BOF classes
must be referenced in the main classpath for the entire application server.

For each of the three common DFC clients discussed below, there is no set
location for BOF code to be deployed. You can choose any directory you want, as long
as the client application’s JVM has access to that directory. Once you deploy
the jar file(s) into the directory of your choice, you must update the client
JVM’s classpath to include a reference to the BOF jar(s).

For a typical WDK environment, the easiest way to ensure that your BOF code will be
found by the DFC classloader is to include your BOF jar file(s) on the same classpath
that includes the DFC jar file (dfc.jar). For example, an Apache Tomcat
application server will have a batch file (or a shell script) that is used to setup the
classpath and launch the server. To include your BOF code on the classpath, all you
have to do is update it to include a reference to the location where your BOF jar(s)
are deployed. Other application servers may have batch files, configuration files, or
configuration consoles through which you can update the classpath.

Another environment where BOF code often needs to be deployed is the Documentum
Java Method Server. If your BOF code needs to be used by the method server, it must be
deployed to this machine. The Java Method Server is actually a Tomcat instance which
typically runs on the content server machine (though not necessarily), but rather than
relying on the familiar catalina.bat (or catalina.sh) scripts
to start up, it is configured as a service in the Windows registry. In a Windows
environment, the classpath it uses comes from a Windows registry setting, which can be
found in the following location:

HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesDmJavaMethodServerParameters

In a Web Publisher environment, you may also need for your BOF code to be deployed
to the Site Caching Services (SCS) Source, which is also a Tomcat instance that runs on
the content server. On a Windows machine, the configuration for this service can be
found in the following location (where the port number of the SCS source in this case
was 6677):

HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesDocumentum SCS_source_6677Parameters

Remember, the only way for either of these applications (or any other application)
to leverage your BOF code is for your BOF jars to be deployed on the application’s
classpath, and for the BOF implementation classes to be registered in DBOR on the
machine where the BOF code is deployed.

5.3 and later: Hosting BOF code in the Docbase

In DFC 5.3, Documentum has introduced a new way of managing BOF code that is
far superior to the DBOR. BOF code is now stored in the repository and dynamically
loaded by DFC as needed by application code running against that docbase. SBOs can
be hosted in a single “global” repository, providing the same functionality to
multiple docbases; TBOs can now be hosted and configured in only the docbase that
uses them, allowing for different TBO registrations to be set up for different
docbases, even if they are being accessed by a single client application (something that simply wasn’t
provided for in the pre-5.3 BOF framework).

Chapter 3 of the DFC 5.3 Development Guide provides an excellent overview of this
new BOF architecture, and the reader is encouraged to read this through a couple
times to gain some familiarity with the new concepts.

JARs, Libraries, and Modules

With 5.3, Documentum has introduced the concept of a “module”, which is a unit
of executable code that is stored in the docbase. Service Based and Type Based Objects
are one kind of module. To support this new module framework, a few new object types
have been introduced: dmc_jar, dmc_module, and dmc_java_library.

The dmc_jar object type is a subtype of dm_document, and it is used for JAR
archives containing Java code. This might include a JAR containing your TBO
implementation class(es), or perhaps your SBO interface class(es), or even
a third-party library that is leveraged by your SBO. How a dmc_jar object is
used depends on whether it is a part of a module or a Java library.

The dmc_java_library object type is a subtype of dm_folder. It is a special
folder type which has additional attributes indicating how the JARs within
should be configured by the DFC class loader. In DFC 5.3, the only additional
attribute is whether or not the Java code within should be “sandboxed”, meaning
isolated from other, possibly conflicting, Java code in the classpath.

The dmc_module object type is a subtype of dm_folder. It is similar to the
dmc_java_library object type, in that it is a special kind of folder with
additional attributes governing how the member objects should be used.
The contents can be dmc_jar objects containing the BOF code, as well
as dmc_java_libraries containing any libraries the BOF module code depends on.
The metadata loosely corresponds to a single entry in DBOR, specifying
whether the module is an SBO or a TBO, and how the code it contains should
be run, what other modules it depends on, etc. Unlike a dmc_java_library,
a dmc_module is always sandboxed.

There can be other types of modules in DFC 5.3. Methods can now be
implemented by repository-based module code. Documentum’s new (limited)
support for aspects is also implemented using repository-based module code.

Although the back-end mechanics are important to understand, Documentum insulates the
developer from most of this: the recommended process for creating and managing BOF
objects in the docbase is to use Documentum Application Builder (DAB) 5.3, which
provides a way of packaging modules in a Docapp. Consequently, the creation and
management of modules and java libraries may actually require little understanding
of how module code is actually represented in the repository; however, it helps
to know what is going on behind the scenes — it will make your life easier when
the time comes to debug!

Registries

Documentum uses the term registry to describe the accumulated
configuration information of all the various modules and libraries that are
registered in a repository. When you install a module into a docbase, you
are ‘registering’ that module in that repository.

A TBO is specific to its repository; that is to say, a TBO must be deployed
into the repository that you want it to be used with, and it will only be
available to code operating on that repository. To put this another way, a
TBO must always appear in the registry of the repository that it will be
used for.

An SBO, however, is more likely to contain code that needs to be shared
across multiple repositories. Rather than requiring this SBO code to be
deployed into every single repository, you can specify one repository’s
registry as the global registry, and that repository can
provide the SBO module code for client application code running against
many other docbases.

In order for an application to be able to access the global repository,
it needs user login credentials. Documentum recommends you create a
specific BOF user for this purpose, and indeed the installation can do
this for you. The user credentials are configured in the familiar
dfc.properties file. A single DFC installation can have only one
global repository, but a repository can serve as the global repository
for multiple DFC installations.

For SBOs, the interface JARs still must be deployed into the client
application environment, even though nothing needs to be registered
in DBOR. In practice, the author has found in convenient enough to
simply deploy the BOF interfaces as part of the overall WDK application
code, so that they are automatically available without the need for
building and deploying a separate BOF interface JAR. If you package
code separately for deployment to the method server, then you will want
to be certain to include the BOF interface classes, either as their
own JAR or as part of the method jar(s).

Debugging

Although BOF code is provided to the JVM dynamically, it still runs inside the JVM
the same as any other Java code, and so you can debug into it the same way. Debugging BOF
code is as simple as attaching a debugger to the JVM your BOF code is running in, whether
it is your own application server or even the Java Method Server (which is just an apache
tomcat server).

Logging

Logging from BOF is no different from logging from other DFC-based code. It is
recommended that you use DfLogger. You can control how the logs are generated using
the familiar log4j.properties file.

Exceptions

It is recommended that all BOF methods escalate any DfExceptions which are
thrown by code that they call. This vastly simplifies development.

Example 1

Implement a TBO customization to log a warning message when a document of
a certain type is checked out.

Suppose that we need to log a warning message whenever objects of
a certain type are checked out. The warning message should include the
email address of the lock owner, so that system administrators can have
a simple external record of who has checked the document out. It is a
somewhat contrived example, but it is easy to imagine an email service in
place of the simple logging; or perhaps we might call out to a web service
somewhere to synchronize checkouts with an external system.

With BOF, you can implement a TBO that automatically logs the message when a
document of the specified type is checked out.

Implementation

The TBO class, which we will call BfExampleOneTbo, must override
the doCheckout method. In the overridden method’s code, it will call
the supertype’s doCheckout method first, to ensure that the document
is checked out. Then it will look up the the lock owner’s email address
within Documentum, and then log the checkout activity using the DfLogger.warn
method.

The file below provides a basic example of the TBO class described above.

Deployment

The TBO class is completely self-contained, with no dependencies on
anything other than DFC. We have overridden only existing methods, and have
not added any custom methods to the object, so there is no need for an
interface class. Deploying this TBO is as simple as adding it to a JAR
file and then packaging that JAR as a module in Documentum Application Builder.

The JAR file below includes the compiled BfExampleOneTbo class.

To install this simple TBO into the repository, take the following steps
in Documentum Application Builder.

    1. Create a new docapp in the repository, named BOF Examples.
    2. Insert a new object type, named bf_example, as a subtype of dm_document.

bf_example_object_type
Figure 1:
bf_example object type properties in Application Builder

    1. Check the docapp in, then check it out again. This will ensure that the
      object type can be created without any problem.
    2. Insert a new module, named bf_example, with type TBO. A TBO module’s
      name must be the name of the object_type that should use this TBO code.

bf_example_object_type

Figure 1:

BfExampleOneTbo_module_implementation
Figure 2:
bf_example TBO module implementation properties in Application Builder

  1. Check the docapp in. This should automatically install the TBO in the
    repository.

Assuming your Log4J configuration is set to log warning message somewhere,
you should soon begin seeing the warning message when objects of type bf_example
are checked out.

Example 2

Implement an SBO customization to provide logging capability to multiple TBOs.

The previous example illustrated how simple it is to implement a rudimentary
TBO. But in the real world, your TBO may require functionality that is more
generic and should be represented somewhere other than the TBO. Or there
could be functionality that more than one TBO needs to execute. Rather than
implementing this in your TBOs, you can extract this common functionality
into an SBO. Let’s take the previous example one step further, to illustrate
how the same functionality could be implemented by a TBO that calls out to
an SBO.

Implementation

For this example, we will extract the logging functionality into a logging
service BfLogService, which will implement the interface IBfLogService. Although
we will still only be implementing the one method for logging a warning about
checkout, it is easy to imagine a more complex and complete service.

The rule for SBO development is that you must have an SBO interface and implementation
class. Our SBO interface will be extremely simple: aside from the standard TBO
methods from IDfBusinessObject, it will have one method: logCheckout. This method
will take in an IDfDocument, look up the lock owner’s email address, and log the
warning with DfLogger.

The files below provide a basic examples of the interface and implementation
class described above.

Now we must update our TBO implementation to use the SBO. The new class
BfExampleTwoTbo provides the new implementation of the doCheckout method,
which instantiates the IBfLogService SBO and calls its logCheckout method.

The file below provides a basic example of the updated TBO class described
above.

Deployment

The solution for Example 2 is actually made up of two separate modules: the TBO
and the SBO. Each one will need to be set up in Application Builder. First we will
insert a new SBO module into the docapp, and then we will pick up where we left off
in Example 1, modifying the existing TBO configuration for the bf_example type to
use our new BfExampleTwoTbo class instead of the old BfExampleOneTbo class.

The SBO must be deployed as two separate JAR files: one containing the interface,
the other containing the implementation.

The JAR files below include the compiled classes for IBfLogService and BfLogService,
respectively.

To install the SBO into the repository, follow these steps:

    1. Checkout the BOF Examples docapp.
    2. Insert a new module, named com.bluefishgroup.sample.bof.sbo.IBfLogService,
      with type SBO. An SBO module’s name is by convention the fully qualified name
      of the SBO interface it implements. The interface and implementation JARs
      can be uploaded from your local machine.

BfExampleTwoTbo_module_dependencies
Figure 3:
IBfLogService SBO module implementation properties in Application Builder

  1. Checkin the docapp.

To update the existing TBO configuration so that it uses the new BfExampleTwoTbo
class, follow these steps:

    1. Checkout the BOF Examples docapp.
    2. Edit the bf_example TBO module, removing the old example1.jar and adding a new
      example2.jar. DAB may warn you that the specified class name cannot be found (since
      you removed the JAR containing this class). Ignore this, and simply select the new
      BfExampleTwoTbo class from the dropdown, as shown.

IBfLogService_module_implementation
Figure 4:
bf_example TBO module implementation properties in Application Builder

    1. Unlike the TBO from Example 1, this TBO relies on the IBfLogService SBO interface.
      If that interface is not available, BOF will be unable to instantiate and execute
      the TBO. If the SBO interface class is deployed to the client machine, this won’t
      be a problem, as the classloader should be able to find it there. But in our case,
      the TBO is the only code which uses the SBO, so we don’t need to deploy the SBO
      interface into our client application. However, we still must be sure to specify
      to provide the SBO interface to our TBO module, This can be done on the Dependencies
      Tab, by specifying a Required Module.

IBfLogService_module_implementation BfExampleTwoTbo_module_implementation
Figure 5:
bf_example TBO module dependencies in Application Builder

  1. Checkin the docapp.

The new TBO and SBO should be downloaded automatically by DFC and go into
use very quickly, usually within minutes. I have seen some WDK session instability
during these “hot swaps”, but it is usually cleared up by logging out
and logging back in.

At the end of these examples, the docapp hierarchy for the BOF Examples docapp
should look something like the following:

BOF_Examples_docapp_hierarchy
Figure 6:
bf_example object type properties in Application Builder

Further Reading

Be sure to take a look at the following documents, as they provide excellent
background for understanding and using the BOF.

  • Documentum Foundation Classes (DFC) 5.3 Development Guide: Chapter 3 – Using the Business Object Framework (BOF)
  • Documentum 5.3 System Migration Guide: Chapter 19 – Migrating DFC or BOF-based Custom Applications
  • Documentum Developer Program – BOF Component Development Guidelines
  • Documentum Business Objects Framework – White Paper (February 2003)

Like this article?

Share on facebook
Share on Facebook
Share on twitter
Share on Twitter
Share on linkedin
Share on Linkdin
Share on pinterest
Share on Pinterest

Leave a comment