New York University

Computer Science Department

Courant Institute of Mathematical Sciences

 

JNDI

       

Course Title: Application Servers                                     Course Number: g22.3033-011

Instructor: Jean-Claude Franchitti                                      Session: 5

 

If a distributed application's components can't locate one another, then they can't work together. Therefore, distributed applications require, almost by definition, something to help the components to find each other. The Java Naming and Directory Interface (JNDI) provides just this capability. The Java Naming and Directory InterfaceTM (JNDI) 1.2 is a major new upgrade release that adds new functionality to the basic naming and directory support offered in the 1.1.x releases. New features include event notification, and LDAPv3 extensions and controls

A card catalog, for the uninitiated, maps the names of books to their location in the library. As surprising as it may seem, the notion of a card catalog is quite handy in the world of computing, as well. In computing, we call it a naming service, which associates names with the locations of services and with information. It provides computer programs with a single location where they can find the resources they need.

Finding resources is of particular importance in large-scale enterprise environments, where the applications you build may depend on services provided by applications written by other groups in other departments. A well-designed naming infrastructure makes such projects possible -- and the lack of one makes them impossible. In fact, many business-process reengineering efforts begin with the design and implementation of a robust, enterprise-wide naming and directory infrastructure.

The Java Naming and Directory Interface (JNDI). JNDI provides a common-denominator interface to many existing naming services. As such, JNDI was not designed to replace existing technology; instead, it provides a common interface to existing naming services. Let's begin by taking a look at some of these services.

 

 

 

 

I. An introduction to naming services



The figure below depicts the organization of a generic naming service.

 

 

 


A naming service maintains a set of bindings. Bindings relate names to objects. All objects in a naming system are named in the same way (that is, they subscribe to the same naming convention). Clients use the naming service to locate objects by name.

There are a number of existing naming services, a few of which are described below. They each follow the pattern above, but differ in the details.

COS (Common Object Services) Naming: The naming service for CORBA applications; allows applications to store and access references to CORBA objects.

DNS (Domain Name System): The Internet's naming service; maps people-friendly names (such as www.etcee.com) into computer-friendly IP (Internet Protocol) addresses in dotted-quad notation (207.69.175.36). Interestingly, DNS is a distributed naming service, meaning that the service and its underlying database is spread across many hosts on the Internet.

LDAP (Lightweight Directory Access Protocol): Developed by the University of Michigan; as its name implies, it is a lightweight version of DAP (Directory Access Protocol), which in turn is part of X.500, a standard for network directory services. Currently, over 40 companies endorse LDAP.

NIS (Network Information System) and NIS+: Network naming services developed by Sun Microsystems. Both allow users to access files and applications on any host with a single ID and password.

 

Common features

As mentioned earlier, the primary function of a naming system is to bind names to objects (or, in some cases, to references to objects -- more on which in a moment). In order to be a naming service, a service must at the very least provide the ability to bind names to objects and to look up objects by name.

Many naming systems don't store objects directly. Instead, they store references to objects. As an illustration, consider DNS. The address 207.69.175.36 is a reference to a computer's location on the Internet, not the computer itself.

JNDI provides an interface that supports all this common functionality.

 

Their differences


It's also important to understand how existing naming services differ, since JNDI must provide a workable abstraction that gets around those differences.

Aside from functional differences, the most noticeable difference is the way each naming service requires names to be specified -- its naming convention. A few examples should illustrate the problem.

In DNS, names are built from components that are separated by dots ("."). They read from right to left. The name "www.etcee.com" names a machine called "www" in the "etcee.com" domain. Likewise, the name "etcee.com" names the domain "etcee" in the top-level domain "com."

In LDAP, the situation is slightly more complicated. Names are built from components that are separated by commas (","). Like DNS names, they read from right to left. However, components in an LDAP name must be specified as name/value pairs. The name "cn=Todd Sundsted, o=ComFrame, c=US" names the person "cn=Todd Sundsted" in the organization "o=ComFrame, c=US." Likewise, the name "o=ComFrame, c=US" names the organization "o=ComFrame" in the country "c=US."

As the examples above illustrate, a naming service's naming convention alone has the potential to introduce a significant amount of the flavor of the underlying naming service into JNDI. This is not a feature an implementation-independent interface should have.

JNDI solves this problem with the Name class and its subclasses and helper classes. The Name class represents a name composed of an ordered sequences of subnames, and provides methods for working with names independent of the underlying naming service.

A look at JNDI naming


As mentioned above, it's important to remember that JNDI is an interface rather than an implementation. This fact has some disadvantages -- you need access to an existing naming service (such as an LDAP service) and you need to understand something about how it works in order to play with JNDI. On the other hand, it does allow JNDI to integrate seamlessly into an existing computing environment where an established naming service holds sway.

JNDI naming revolves around a small set of classes and a handful of operations. Let's take a look at them.

Context and InitialContext


The Context interface plays a central role in JNDI. A context represents a set of bindings within a naming service that all share the same naming convention. A Context object provides the methods for binding names to objects and unbinding names from objects, for renaming objects, and for listing the bindings.

Some naming services also provide subcontext functionality. Much like a directory in a filesystem, a subcontext is a context within a context. This hierarchical structure permits better organization of information. For naming services that support subcontexts, the Context class also provides methods for creating and destroying subcontexts.

JNDI performs all naming operations relative to a context. To assist in finding a place to start, the JNDI specification defines an InitialContext class. This class is instantiated with properties that define the type of naming service in use and, for naming services that provide security, the ID and password to use when connecting.

For those of you familiar with the RMI Naming class, many of the methods provided by the Context interface outlined below will look familiar. Let's take a look at Context's methods:

void bind(String stringName, Object object): Binds a name to an object. The name must not be bound to another object. All intermediate contexts must already exist.

void rebind(String stringName, Object object): Binds a name to an object. All intermediate contexts must already exist.

Object lookup(String stringName): Returns the specified object.

void unbind(String stringName): Unbinds the specified object.

The Context interface also provides methods for renaming and listing bindings.

void rename(String stringOldName, String stringNewName): Changes the name to which an object is bound.

NamingEnumeration listBindings(String stringName): Returns an enumeration containing the names bound to the specified context, along with the objects and the class names of the objects bound to them.

NamingEnumeration list(String stringName): Returns an enumeration containing the names bound to the specified context, along with the class names of the objects bound to them.

Each of these methods has a sibling that takes a Name object instead of a String object. A Name object represents a generic name. The Name class allows a program to manipulate names without having to know as much about the specific naming service in use.

 

The example


The example below illustrates how to connect to a naming service, list all of the bindings, or list a specific binding. It uses the filesystem service provider, which is one of the reference JNDI service-provider implementations provided by Sun. The filesystem service provider makes the filesystem look like a naming service (which it is, in many ways -- filenames like /foo/bar/baz are names and are bound to objects like files and directories). I selected it because everyone has access to a filesystem (as opposed to, say, an LDAP server).

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Binding;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;

import java.util.Hashtable;

public
class Main {

public
static
void
main(String [] rgstring) {
try {
// Create the initial context. The environment
// information specifies the JNDI provider to use
// and the initial URL to use (in our case, a
// directory in URL form -- file:///...).
Hashtable hashtableEnvironment = new Hashtable();
hashtableEnvironment.put(
Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory"
);
hashtableEnvironment.put(
Context.PROVIDER_URL,
rgstring[0]
);
Context context = new InitialContext(hashtableEnvironment);
// If you provide no other command line arguments,
// list all of the names in the specified context and
// the objects they are bound to.
if (rgstring.length == 1) {
NamingEnumeration namingenumeration = context.listBindings("");
while (namingenumeration.hasMore()) {
Binding binding = (Binding)namingenumeration.next();
System.out.println(
binding.getName() + " " +
binding.getObject()
);
}
}
// Otherwise, list the names and bindings for the
// specified arguments.
else {
for (int i = 1; i < rgstring.length; i++) {
Object object = context.lookup(rgstring[i]);
System.out.println(
rgstring[i] + " " +
object
);
}
}
context.close();
}
catch (NamingException namingexception) {
namingexception.printStackTrace();
}
}
}

The program in the listing above first creates an initial context from the specified JNDI provider (in this case, Sun's filesystem provider) and a URL specifying a local directory. If no additional command-line arguments are specified, the program lists the objects and names of every entity in the specified directory. Otherwise, it lists the objects and names of only those items specified on the command line.

You should now have both an understanding of and an appreciation for naming services in general and JNDI in particular. In distributed environments, they are valuable tools for locating information and resources. JNDI makes it possible to work with a variety of naming services without having to master a multitude of APIs. Next month, we'll take a look at the other half of JNDI -- its directory functions

 

II. An introduction to directory services

As applications become more distributed, the efficient management and distribution of the information upon which they depend becomes more and more of a problem. Directory services such as LDAP (the Lightweight Directory Access Protocol) address this problem. For Java applications, the Java Naming and Directory Interface (JNDI) provides a generic interface to LDAP and other directory services.

In a library, the card catalog maps the names of books to references to their locations on the shelves; in a distributed system, the naming service maps the names of objects to references to their locations in a network. Both tools are extremely useful in their respective domains.

It turns out that, with little effort, we can stretch this analogy even farther. You see, a card in a card catalog contains more than just a book's location; it also contains the author's name, the date the book was published, the book's length, and many other useful bits of information. You can find out a lot about the book without actually having to go and get it.

In the domain of computer software, tools that provide this type of information are known as directories. A directory service provides a way to manage the storage and distribution of shared information. Such information can range from the email addresses and phone numbers of a company's employees, to the IP addresses and print capabilities of a department's printers, to the configuration information for a suite of application servers.

The figure below depicts a generic directory service.


 


A directory service manages a directory of entries. A directory entry can refer to a person, place, service, or almost any other concrete object or abstract concept. An entry also has attributes associated with it; an attribute consists of a name or identifier and one or more values. These attributes describe the entry, and the exact set of attributes depends on the type of the entry. For example, the entry for an individual might have the following attributes (note the two email addresses):

Name: John Doe
Address: 123 Somewhere Street
Email: john@xyz.com
Email: jdoe@abcd.com

Directory services are simple databases. Like their relational cousins, many common directory services provide search and filter functionality. Instead of locating an entry only by name, these directory services allow you to locate entries based on a set of search criteria.

Naming services and directory services are logical partners. In fact, most existing products provide both sets of functionality. Naming services provide name-to-object mapping, and directory services provide information about the objects and tools for searching for them.

There are a number of existing directory service products; LDAP (the Lightweight Directory Access Protocol) is the most common. LDAP provides both naming and directory functionality. Numerous commercial implementations exist, as well as some freely available implementations (OpenLDAP being the most common).

 

A look at JNDI directory services


JNDI directory-service support is both comprehensive and powerful. It adds advanced functions, like storing and retrieving serialized class instances, to searching and other components of the basic suite of direct functions. With this thought in mind, let's examine the DirContext class -- the heart of JNDI directory services.

 

Working with attributes


The DirContext class is a subclass of the Context class. It provides all of the standard naming service functionality, and can also work with attributes and search for directory entries.

Let's take a look at the methods of DirContext that extend those methods provided by the Context class.

The bind() method
void
bind(
String stringName,
Object object,
Attributes attributes
)

The bind() method binds a name to an object and stores the specified attributes with that entry. This operation generally tries to preserve existing attributes in cases in which that makes sense. Specifically, if attributes is null and object is an instance of the DirContext class, the resulting binding will retain the attributes originally associated with object.

The rebind() method
void
rebind(
String stringName,
Object object,
Attributes attributes
)

The rebind() method binds a name to an object and stores the specified attributes with that entry. The previous binding is replaced. As is the case with bind(), this operation tries to preserve existing attributes in cases in which that would make sense.

The createSubcontext() method
DirContext
createSubcontext(
String stringName,
Attributes attributes
)

The createSubcontext() method creates a new subcontext and binds a name to it, and then stores the specified attributes with that entry. If attributes is null, this method works in exactly the same fashion as the like-named method on the Context class.

Let's next consider those of DirContext's methods that do not extend methods provided by the Context class, providing the tools for working with attributes instead.

The getAttributes() methods
The class provides two flavors of this method:

Attributes
getAttributes(
String stringName
)

And:

Attributes
getAttributes(
String stringName,
String [] rgstringAttributeNames
)

The getAttributes methods return the attributes associated with the specified entry. The Attributes class represents a collection of attributes; it contains instances of the Attribute class, which by itself represents a single attribute. The first flavor of this method returns all attributes, and the second returns the attributes named in the supplied array of attribute names.

The modifyAttributes methods
modifyAttributes comes in two flavors as well:

void
modifyAttributes(
String stringName,
int nOperation,
Attributes attributes
)

And:

void
modifyAttributes(
String stringName,
ModificationItem [] rgmodificationitem
)

The modifyAttributes methods modify the attributes associated with the specified entry. The permitted operations are ADD_ATTRIBUTE, REPLACE_ATTRIBUTE, and REMOVE_ATTRIBUTE. The first flavor of this method modifies several attributes in the same way, while the second performs a series of modifications on one or more attributes.

As with the Context class, each of the methods above also has a variant that takes a Name object rather than a String object.

Searching
In order to make use of directory service functionality, it is necessary to have some way to search the contents of a directory service. The DirContext class provides two general models by which searches may be conducted.

Searching by attribute name
There are two ways to conduct a search following this model:

NamingEnumeration
search(
String stringName,
Attributes attributesToMatch
)

And:

NamingEnumeration
search(
String stringName,
Attributes attributesToMatch,
String [] rgstringAttributesToReturn
)

In this first model, the search occurs within a single context for entries that contain a specific set of attributes. For the entities that match, the search retrieves either the entire set of attributes (if the search is implemented using the first block of code above) or a select set of attributes (if the search is implemented using the second).

Now, let's look at the second model.

Searching by RFC 2254 filter
Again, there are two ways to implement this model:

NamingEnumeration
search(
Name stringName,
String stringRFC2254Filter,
SearchControls searchcontrols
)

And:

NamingEnumeration
search(
Name stringName,
String stringRFC2254Filter,
Object [] stringRFC2254FilterArgs,
SearchControls searchcontrols
)

In our second model, the search occurs within a context for entries that satisfy a search filter. RFC 2254 (which describes a string representation for LDAP search filters)

An instance of the SearchControls class controls key aspects of the search:

SearchControls(
int nSearchScope,
long nEntryLimit,
int nTimeLimit,
String [] rgstringAttributesToReturn,
boolean boolReturnObject,
boolean boolDereferenceLinks
)

The constructor above lists all of the aspects of a search that a SearchControls instance affects. Corresponding accessors (get and set methods) also exist. Below, I've listed each of these aspects and a short description of each:

nSearchScope: Sets the scope of the search to either the object (OBJECT_SCOPE), the object and the level immediately below it (ONELEVEL_SCOPE), or the object and its entire subtree (SUBTREE_SCOPE).

nEntryLimit: Sets the maximum number of entries that the search will return.

nTimeLimit: Sets the maximum number of milliseconds that the search will run.

rgstringAttributesToReturn: Determines which attributes should be returned along with the entries returned by the search.

boolReturnObject: Determines whether or not the objects bound to selected entries should to be returned along with the entries returned by the search.

boolDereferenceLinks: Determines whether or not links should be dereferenced links (or followed to their ultimate destination) during the search. A link references another directory entry and can span multiple naming systems. The underlying JNDI service provider may or may not provide support for links.

Once again, each of the methods above also has a variant that takes a Name object rather than a String.

If you've made it this far, you should now have a solid understanding of both naming and directory services, and a general understanding of JNDI.

 

III. Using JNDI to store your distributed applications' objects

A JNDI service can be much more than a computerized card catalog. JNDI's object-storage capabilities allow it to play the role of resource administrator in your distributed applications and to provide simple, manageable object persistence. In recognition of its potential, the architects of key Java technologies such as JDBC and EJB built JNDI into their specifications.

The Java Naming and Directory Interface plays an important role in several Java technologies. We're going to take a look at this role to better understand JNDI's strategic position in the overall Java picture. Next, in recognition of your need for a working JNDI service to play with, this handout introduces you to a freely available, portable LDAP implementation, and will teach you how to connect to and use a JNDI service provider. Finally, this handout will take you in for a close look at binding objects to entries in JNDI.

Let's begin with a look at how JNDI appears in other Java technologies.

JNDI everywhere


JNDI plays a role in a number of Java technologies. Let's consider three of them: JDBC (the Java Database Connectivity package), JMS (the Java Messaging Service), and EJB (Enterprise JavaBeans).

JDBC is the Java technology for relational databases. JNDI first appeared in the JDBC 2.0 Optional Package in conjunction with the DataSource interface. A DataSource instance, as its name implies, represents a source of data -- often from a database but not always. A DataSource instance stores information about a data source -- such as its name, the driver to load and use, and its location -- and allows an application to obtain a connection to the data source without regard to the underlying details. The JDBC specification recommends using JNDI to store DataSource objects.

JMS is the Java technology for messaging. The JMS specification describes administered objects -- objects that contain JMS configuration information and are used by JMS clients to locate specific message queues and topics. As is the case with JDBC, the specification recommends locating JMS administered objects via JNDI.

Finally, consider Enterprise JavaBeans. All enterprise beans publish a home interface -- the single location through which clients locate a specific enterprise bean -- via JNDI.

What does JNDI bring to the table that causes it to be so highly regarded?

First, JNDI promotes the notion of a centrally managed information source -- a key requirement for enterprise applications. A centrally managed information source is easier to administer than a distributed collection of information sources. It's also simpler for clients to locate needed information if they only have to look in one place.

Second, as you shall see, JNDI's ability to directly store Java objects allows it to integrate almost transparently into Java applications.

The point of the provider


To use JNDI, you need a naming and directory service and a JNDI service provider. Sun supplies several providers of common naming and directory services (COS naming, NIS, the RMI registry, LDAP, and more). I've settled on LDAP.

LDAP (Lightweight Directory Access Protocol) has the dual advantages of being widely implemented (both in commercial and free forms) and being reasonably easy to use. Its features are also well supported by Sun's LDAP service provider and JNDI.

Since obtaining and configuring an LDAP server isn't really a Java subject matter, I'll only get you headed in the right direction and supply you with references to Internet resources.

Numerous LDAP implementations are available. Many are commercial products such as the Netscape Directory Server and IBM's Secure Way Directory. Some are packaged as part of larger offerings (Microsoft's Active Directory is part of Windows 2000). If you have access to such an implementation, you can skip most of this section. Otherwise, I'm going to describe OpenLDAP -- a freely available implementation of LDAP based on the University of Michigan's reference implementation -- as well as its installation and configuration.

OpenLDAP is available from the OpenLDAP Foundation. Its license is based on Perl's "artistic license," which means that OpenLDAP is free (or open source) software. Prepackaged binaries are available for various flavors of Linux (Debian, Red Hat) as well as BSD Unix. Work is under way on a port to Windows NT.

If you plan to install OpenLDAP, you should read the SLAPD and SLURPD Administrator's Guide (slapd is the name of the LDAP server executable and slurpd is the name of the LDAP replication server).

One final suggestion to make your entire experience more pleasing: no matter which LDAP implementation you use, turn schema checking off. An LDAP schema, like a database schema, defines constraints on the stored information. In normal use, schema checking helps ensure that entries (think of address book entries) conform to the correct format. However, since you'll probably be playing rather than building something of lasting significance, schema checking will just get in the way.

Connecting to a JNDI context

It was mentioned previously that you need an initial context to do JNDI operations. Before you can do anything with JNDI, you need an initial context. All operations are performed relative to the context or one of its subcontexts.

Obtaining an initial context requires three steps:

First, select a service provider. If you're going to use OpenLDAP or some other LDAP implementation, Sun supplies a reference LDAP service provider. Add the name of the service provider to the set of environment properties (stored in a Hashtable instance):

Hashtable hashtableEnvironment = new Hashtable();
hashtableEnvironment.put(
Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory"
);

Add any extra information the service provider requires. For LDAP, that includes the URL that identifies the service, the root context, and the name and password to connect with:

// the service: ldap://localhost:389/
// the root context: dc=etcee,dc=com
hashtableEnvironment.put(
Context.PROVIDER_URL,
"ldap://localhost:389/dc=etcee,dc=com"
);
hashtableEnvironment.put(
Context.SECURITY_PRINCIPAL,
"name"
);
hashtableEnvironment.put(
Context.SECURITY_CREDENTIALS,
"password"
);

Finally, get the initial context. If you just intend to perform naming operations, you'll only need a Context instance. If you intend to perform a directory operation as well, you'll need a DirContext instance instead. Not all providers supply both:

Context context = new InitialContext(hashtableEnvironment);

Or:

DirContext dircontext = new InitialDirContext(hashtableEnvironment);

That's all there is to it. Now let's look at how applications store objects to and retrieve objects from JNDI.

Work with objects


The ability to store Java objects is useful: object storage provides persistence and allows objects to be shared between applications or between different executions of the same application.

From the standpoint of the code involved, object storage is surprisingly easy:

context.bind("name", object)

The bind() operation binds a name to a Java object. The syntax of the command is reminiscent of RMI, but the semantics are not as clearly defined. It's permissible for the bind() operation to store either a snapshot of the object or a reference to a "live" object, for example.

Be aware that the bind() operation throws a NamingException if an exception occurs during the execution of the operation.

Now let's take a look at the bind() operation's complement -- lookup():

Object object = context.lookup("name")

The lookup() operation retrieves the object bound to the specified name. Once again, the syntax is reminiscent of RMI, but the method's semantics are not as clearly defined.

Just as with bind(), the lookup() operation throws a NamingException if an exception occurs during the execution of the operation.

Object storage


What does it mean to store an object in a JNDI naming and directory service? We've already mentioned that the exact semantics of the bind() and lookup() operations aren't tightly defined; it's up to the JNDI service provider to define their semantics.

According to the JNDI specification, service providers are encouraged (but not required) to support object storage in one of the following formats:

Serialized data

Reference

Attributes in a directory context

If all JNDI service providers support these standard mechanisms, Java programmers are free to develop generic solutions that work even when the underlying service provider layer changes.

Each of the methods above has advantages and disadvantages. The best method will depend on the requirements of the application under development.

Let's consider each in turn.

As serialized data


The most obvious approach to storing an object in a directory is to store the serialized representation of an object. The only requirement is that the object's class implement the Serializable interface.

When an object is serialized, its state becomes transformed into a stream of bytes. The service provider takes the stream of bytes and stores it in the directory. When a client looks up the object, the service provider reconstructs it from the stored data.

The following code demonstrates how to bind a LinkedList to an entry in an JNDI service:

// create linked list
LinkedList linkedlist = new LinkedList();
.
.
.
// bind
context.bind("cn=foo", linkedlist);
.
.
.
// lookup
linkedlist = (LinkedList)context.lookup("cn=foo");

It's that easy!

Unfortunately, the other two methods are more complicated.

As a reference

Sometimes it's not appropriate (or possible) to serialize an object. If the object provides a service on a network, for example, it doesn't make sense to store the state of the object itself. We're interested in the information necessary to find and communicate with the object.

An example is a connection to an external resource (one outside the scope of the Java Virtual Machine) such as a database or file. It clearly doesn't make sense to try to store the database or the file itself in the JNDI service. Instead, we want to store the information necessary to reconstruct the connection.

In this case the programmer should either bind a Reference instance that corresponds to the object or have the object's class implement the Referenceable interface (in which the object generates and provides a Reference instance when requested by the service provider).

The Reference instance contains enough information to recreate the reference. If a reference to a file was stored, the reference contains enough information to create a File object that points to the correct file.

As attributes


If you're using a service provider that provides directory functionality instead of only naming functionality, you can also store an object as a collection of attributes on a DirContext object (a DirContext instance differs from a Context instance in that it may have attributes).

To use this method, you must create objects that implement the DirContext interface and contain the code necessary to write their internal state as an Attributes object. You must also create an object factory to reconstitute the object.

This approach is useful when the object must be accessible by non-Java applications.

 

IV. An example

 

Doc-u-Matic is an application that demonstrates clearly the strength of JNDI. Doc-u-Matic implements a document publication and distribution infrastructure. You'll see how a naming and directory service, combined with JNDI's ability to store Java objects, leads to simpler -- yet more powerful -- distributed applications.

The Doc-u-Matic application illustrates JNDI's support for:

Centralized information administration

Network-wide information distribution

Object persistence

Keep the objects around


Doc-u-Matic is first-and-foremost a demonstration of JNDI-supported object persistence. Therefore, I think it makes sense to begin with a review of JNDI's support for stored objects.

There are three techniques for storing Java objects in a JNDI service: as serialized data, as a reference to an object, and as the attributes on a directory context. Storing an object as serialized data is the simplest of the three techniques. Storing an object as a reference is useful in situations in which it doesn't make sense (or isn't possible) to store actual objects. Finally, storing objects as attributes on a directory context is useful when other, non-Java applications need access to the object's information. The application developed uses the first two methods of object storage.

The functional units


Figure 1, below, illustrates Doc-u-Matic from a functional perspective. Doc-u-Matic consists of three major functional units: the JNDI service (of course), a library service, and one or more clients.


 

 


A library is a place to which you publish objects and from which you retrieve them. An object can be any Java instance, as long as it supports one of the storage methods mentioned above -- it can be a String, a JavaBean, and so on.

JNDI plays two roles in Doc-u-Matic. It provides a single, well-known location where clients can locate the library service (standard address-book functionality), and it supports the implementation of the library service itself. On the latter point, it's worth noting that nothing in the design of a library implies that it has to be implemented on top of JNDI -- that's just the direction I took (since this is an article on JNDI). You could implement a library on top of any technology that provides support for publishing and retrieving information, including HTTP, JDBC, NNTP, or IMAP. Best of all, the clients would never know the difference.

Before you proceed...


Doc-u-Matic assumes you have obtained, or have access to, and have configured an LDAP service. The application should support any naming and directory service that provides a JNDI service provider. However, I will assume you're using LDAP.

Before you proceed, make sure you've obtained, installed, and configured the following (if necessary, refer back to my earlier columns):

An LDAP implementation

Sun's JNDI reference implementation and the LDAP service provider

You'll also need to create the initial context that the application will connect to. I assume the following:

ou=HowTo,o=JavaWorld

If you're not sure how to do this, you'll need to refer to your LDAP service's documentation.

Usage

There are two usage roles: the administrator and the user. The administrator (think of a librarian) creates or deploys the library and publishes objects of various types in the library. The administrator also creates and distributes a properties file that contains all of the information necessary to connect to and use the library. Users, on the other hand, retrieve objects from the library.

The administrator's role


Let's begin by assuming the role of the administrator. The deployment tool is named JNDIDeploy. Before you deploy a library, you must create a properties file that contains the information necessary to connect to an initial context. The properties file must also contain the name that the JNDILibrary object will be bound to. If you configured your LDAP service with the LDIF I supplied above, the following properties file will provide a good place to start:

# DEPLOYMENT PROPERTIES
# This properties file contains all of the information necessary to
# find and connect to the JNDI service that holds the library and all
# published objects.
java.naming.factory.initial = com.sun.jndi.ldap.LdapCtxFactory
java.naming.provider.url = ldap://localhost:389/o=HowTo,ou=JavaWorld
library.name = cn=library

The library deploys as follows (I assume you've already set up the classpath to point to the jndi.jar and ldap.jar JAR files):

java JNDIDeploy <properties file>

Once you, as administrator, have deployed a library, you must distribute a properties file to all users. The client applications I've developed all read a properties file pointed to by a URL. Therefore, the easiest way to distribute this file is to put it on a Web server and distribute the URL of the properties file.

Once you have deployed a library, you can publish objects to the library. By objects, I mean instances of Java classes. Objects play the role of abstract containers of information. As such, the simplest object is probably an instance of the String class. I've purposefully placed few requirements on publishable objects; they must be instantiateable via the Beans.instantiate() method, and they must be storable via JNDI. Objects are published as follows:

java Publish <properties URL> [name class]...

The Publish command requires the URL of the properties file mentioned above. It also accepts any number of additional pairs of arguments. The first value in each pair is the name of the object. The second is the name of the class (including package information) that will be instantiated and stored. The name must conform to whatever naming policy the JNDI service provider requires. In the case of LDAP, names will be of the form:

<key>=<value>

The user's role


Now, remove your administrator's hat and don your user's hat. It's time to retrieve some of the objects we've published. As a user, the only facts we have to know to retrieve an object are its name and the URL that describes the location of the library. We don't have to know how the library is implemented. This is especially useful if we, as users, must write programs to use the library. As you'll see a bit later, the code required to use a library is small indeed.

I've supplied a small client program that retrieves objects from the library. Objects are retrieved as follows:

java Client <properties URL> [name]...

The Client command requires the URL of the properties file. It also accepts any number of additional arguments specifying the names of objects to retrieve from the library.

The code


Figure 2, below, depicts the six classes that form the core of Doc-u-Matic. I'll visit each, briefly describe its role, and then present the important bits of the code.


 

 

 


Library.java


The Library interface defines the methods that all libraries must provide. For the sake of simplicity, it requires only two methods: one to publish objects and one to retrieve objects. Full-featured libraries would provide search functionality as well.

/**
* Publishes an object in the library.
*
* @param object the object
* @param stringName the name of the object
* @param map a map containing the object's attributes
*
*/
public
void
publish(Object object, String stringName, Map map);

/**
* Retrieves a copy of a published object from the library
* and restores it if necessary.
*
* @param stringName the name of the object
*
* @returns the object, or null
*
*/
public
Object
retrieve(String stringName);

JNDILibrary.java


The JNDILibrary class provides a JNDI-based implementation of the Library interface. The publish() and retrieve() methods demonstrate how to use JNDI to bind objects to and look up objects from a directory service.

/**
* Publishes an object in the library.
*
* @param object the object
* @param stringName the name of the object
* @param map a map containing the object's attributes
*
*/
public
void
publish(Object object, String stringName, Map map) {
try {
// Use the properties to establish an initial directory context.
DirContext dircontext = new InitialDirContext(m_properties);
// Create an iterator that contains all the entries in the map.
Iterator iterator = map.entrySet().iterator();
// For each entry, create an attribute.
BasicAttributes basicattributes = new BasicAttributes();
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry)iterator.next();
basicattributes.put(entry.getKey().toString(),
entry.getValue().toString());
}
// Bind the object to the specified name.
dircontext.rebind(stringName, object, basicattributes);
// Close the context.
dircontext.close();
}
catch (Exception exception) {
exception.printStackTrace();
}
}

/**
* Retrieves a copy of a published object from the library
* and restores it if necessary.
*
* @param stringName the name of the object
*
* @returns the object, or null
*
*/
public
Object
retrieve(String stringName) {
Object object = null;
try {
// Use the properties to establish an initial directory context.
DirContext dircontext = new InitialDirContext(m_properties);
// Look up the object using the specified name.
object = dircontext.lookup(stringName);
// Close the context.
dircontext.close();
}
catch (Exception exception) {
exception.printStackTrace();
}
return object;
}

The getReference() method demonstrates how to create a Reference instance that represents the current instance of a class. Instead of being stored as serialized data, classes that implement the Referenceable interface are stored indirectly, via Reference instances. Notice how, in the example below, the properties are transformed and stored as a series of bytes. Typically, enough of an object's state must be stored to create a working copy of the object when the object is looked up in a directory service.

/**
* Gets the reference for the library.
*
* @returns the reference
*
*/
public
Reference
getReference() {
Reference reference = null;
try {
// Store the properties as an array of bytes.
ByteArrayOutputStream bytearrayoutputstream = new
ByteArrayOutputStream();
m_properties.store(bytearrayoutputstream, null);
// Create a reference to the library.
reference = new Reference(
JNDILibrary.class.getName(),
new BinaryRefAddr("properties", bytearrayoutputstream.toByteArray()),
JNDILibraryFactory.class.getName(),
null
);
}
catch (Exception exception) {
exception.printStackTrace();
}
return reference;
}

JNDILibraryFactory.java


The JNDILibraryFactory class is used on the client-side to create an instance of the JNDILibrary class when a JNDI library is looked up in a directory service. JNDILibraryFactory's primary operation: transform the stored binary property-file information into live property information. This transformation allows the new instance to function identically to the version that was stored:

/**
* Creates an object instance given a reference.
*
* @param object a reference
* @param name a name
* @param context the context
* @param hashtable the environment
*
* @returns an object, or null
*
*/
public
Object
getObjectInstance(Object object, Name name, Context context, Hashtable
hashtable)
throws Exception {
// Checks whether or not the object is an instance of a reference.
if (object instanceof Reference) {
Reference reference = (Reference)object;
// Checks whether or not it is a valid reference for this factory.
if (reference.getClassName().equals(JNDILibrary.class.getName())) {
// Gets the stored payload.
RefAddr refaddr = reference.get("properties");
if (refaddr != null) {
if (refaddr instanceof BinaryRefAddr) {
BinaryRefAddr binaryrefaddr = (BinaryRefAddr)refaddr;
// Recreates the stored properties.
byte [] rgb = (byte [])binaryrefaddr.getContent();
ByteArrayInputStream bytearrayinputstream = new
ByteArrayInputStream(rgb);
Properties properties = new Properties();
properties.load(bytearrayinputstream);
// Creates a new JNDI library with the stored properties.
return new JNDILibrary(properties);
}
}
}
}
return null;
}

The getObjectInstance() and the getReference() methods, provided by the JNDILibrary class, work as a pair.

JNDIDeploy.java


JNDIDeploy class deploys or creates a new library. The body implements as close to a textbook example of binding via JNDI as you're likely to find:

/**
* Deploys a JNDILibrary instance.
*
* This class should be run as an application. It deploys a
* JNDILibrary instance. It assumes the existence of a
* properly configured JNDI service (typically, but not necessarily,
* running LDAP).
*
* The application requires a single command-line argument:
* the name of a file containing the deployment properties. The
* properties file may contain any valid JNDI property, as well as the
* property "library.name", which must contain the name of the
* library.
*
*/
public
static
void
main(String [] rgstring) {
if (rgstring.length != 1) {
System.out.println("Usage: java JNDIDeploy ");
System.exit(-1);
}
try {
Properties properties = new Properties();
// Load the properties from the specified file.
properties.load(new FileInputStream(rgstring[0]));
// Use the properties to establish an initial directory context.
DirContext dircontext = new InitialDirContext(properties);
// Create the JNDI library instance and initialize it with the
// same set of properties (it will publish objects in the same
// context as itself).
JNDILibrary jndilibrary = new JNDILibrary(properties);
// Get the name of the library.
String stringName = properties.getProperty("library.name");
// Bind the library to the specified name.
dircontext.rebind(stringName, jndilibrary, null);
// Close the context.
dircontext.close();
}
catch (Exception exception) {
exception.printStackTrace();
System.exit(-1);
}
}

Publish.java


The Publish class publishes one or more objects in a library. Once again, you're not likely to find a more textbook example of how to look up an object via JNDI -- in this case the library. Once you've retrieved the library, it can be used to publish objects. Notice how the code doesn't have to know anything about how the library is implemented. You'll also notice that the Beans.instantiate() method instantiates the objects, which facilitates the storage of objects that have been put together in the JavaBeans development tool and serialized to disk:

/**
* Publishes an object.
*
*/
public
static
void
main(String [] rgstring) {
if (rgstring.length < 1) {
System.out.println("Usage: java Publish *lt;URL> [name class]...");
System.exit(-1);
}
try {
Properties properties = new Properties();
// Load the properties from the specified URL.
properties.load(new URL(rgstring[0]).openStream());
// Use the properties to establish an initial directory context.
DirContext dircontext = new InitialDirContext(properties);
// Get the name of the library.
String stringName = properties.getProperty("library.name");
// Look up a library using the specified name.
Library library = (Library)dircontext.lookup(stringName);
// Close the context.
dircontext.close();
// For each command-line argument, instantiate the specified
// object, and publish it to the library.
for (int i = 1; i < rgstring.length; i++) {
Object object = Beans.instantiate(null, rgstring[i + 1]);
library.publish(object, rgstring[i], new HashMap());
i++;
}
}
catch (Exception exception) {
exception.printStackTrace();
System.exit(-1);
}
}

Client.java


The Client class is almost identical to the Publish class, except that it retrieves objects rather than publishes them.

/**
* A simple client.
*
* Demonstrates how to connect to and retrieve from a
* Library instance via JNDI.
*
*/
public
static
void
main(String [] rgstring) {
if (rgstring.length < 1) {
System.out.println("Usage: java Client <URL> [name]...");
System.exit(-1);
}
try {
Properties properties = new Properties();
// Load the properties from the specified URL.
properties.load(new URL(rgstring[0]).openStream());
// Use the properties to establish an initial directory context.
DirContext dircontext = new InitialDirContext(properties);
// Get the name of the library.
String stringName = properties.getProperty("library.name");
// Look up a library using the specified name.
Library library = (Library)dircontext.lookup(stringName);
// Close the context.
dircontext.close();
// For each command-line argument, retrieve the specified object
// from the library.
for (int i = 1; i < rgstring.length; i++) {
Object object = library.retrieve(rgstring[i]);
// If the object is restorable, restore it.
if (object instanceof Restorable) {
Restorable restorable = (Restorable)object;
restorable.restore();
}
}
}
catch (Exception exception) {
exception.printStackTrace();
System.exit(-1);
}
}