JNDI overview, Part 4: the Doc-u-Matic, a JNDI application

Pull together your JNDI knowledge with a JNDI-enabled application

Let's dive in. I've spent the last three months describing the Java Naming and Directory Interface (JNDI). By now, you should feel comfortable with naming and directory services and the operations they support. It's now time to build on that foundation. I've developed a JNDI-enabled document publication and distribution application -- called Doc-u-Matic -- that illustrates and utilizes the material I've presented in the past three How-To Java columns.

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

  • Centralized information administration
  • Network-wide information distribution
  • Object persistence

Note: To download the complete source code for this article, see Resources.

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.

You may recall from March's column that 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 I've 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.

Figure 1. Doc-u-Matic: A functional view

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

Before we dive into the code, let's stop for a moment and look at how to use Doc-u-Matic.

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've supplied (see Resources), 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/ou=HowTo,o=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

Now, let's get to the core of this article -- 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 (see Resources to download each class's complete source code).

Figure 2. Doc-u-Matic's six core classes

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 < properties file >");
      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);
    }
  }

And that's all there is to it!

Conclusion

If you've stayed with me through the four columns in the JNDI series, you're probably slightly out of breath -- I covered a lot of ground in four short columns. However, you should also have a much clearer understanding of naming and directory services, JNDI, its capabilities, and the flexibility it brings to JNDI-enabled enterprise applications. JNDI already plays an important role in several key Java APIs -- you can expect its role to expand.

Todd Sundsted has been writing programs since computers became available in convenient desktop models. Though originally interested in building distributed applications in C++, Todd moved on to the Java programming language when it became the obvious choice for that sort of thing. In addition to writing, Todd is a Java architect with ComFrame Software Corporation.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more