SummaryBy Sameer Tyagi
In this article Sameer Tyagi takes a look at the Java Naming and Directory Interface (JNDI), explaining how you can manipulate Java objects on an LDAP server. He'll show you examples that demonstrate how you can store objects, search for objects, see event handling in LDAP, and much more. (5,000 words)
he Lightweight Directory Access Protocol (LDAP), which traces its roots to the X.500 protocol, was developed in the early 1990s as a standard directories protocol. LDAP defines how clients should access data on the server, not how that data is stored on the server. This allows LDAP to become a frontend to any type of data store.
(Note: To download this article's complete source code, see the Resources section below.)
LDAP's basic structure is based on a simple information tree metaphor called a directory information tree (DIT). Each leaf in the tree is an entry; the first or top-level entry is the root entry. An entry includes a distinguished name (DN) and any number of attribute/value pairs. The DN, which is the name of an entry, must be unique. It shows the relationship between the entry and the rest of the DIT in a manner similar to the way in which a file's full path name shows its relationship with the rest of the files in a filesystem. While a path to a file reads left to right, a DN, in contrast, reads from right to left. Here is an example of an DN:
uid=styagi,ou=people,o=myserver.com
The leftmost part of the DN, called a relative distinguished name
(RDN), is made up of an attribute/value pair. In the above example, this pair
would be uid=styagi
. LDAP attributes often use mnemonics, some
examples of which are listed in Table 1.
o |
Organization |
ou |
Organizational unit |
cn |
Common name |
sn |
Surname |
givenname |
First name |
uid |
Userid |
dn |
Distinguished name |
mail |
Email address |
Information about attributes, attribute matching rules, and relationships between objectclasses are defined in the server's schema. Any attribute can have one or more values, depending on how it is defined the schema. A user, for example, can have more than one email address. There is also a special attribute called an objectclass that specifies the required and allowed attributes for a particular entry. Like objects in Java, objectclasses in LDAP can be extended to retain existing attributes and add new ones.
A naming service associates names with objects and finds objects based on their given names. (The RMI registry is a good example of a naming service.) Many naming services are extended with a directory service. While a naming service allows a lookup of an object based on its name, a directory service also allows such objects to have attributes. As a result, with a directory service we can look up an object's attributes or search for objects based on their attributes.
So where does JNDI fit into this LDAP jargon? JNDI does for LDAP what JDBC does for Oracle -- it provides a standard API for interacting with naming and directory services using a service provider interface (SPI), which is analogous to an JDBC driver. LDAP is a standard way to provide access to directory information. JNDI gives Java applications and objects a powerful and transparent interface to access directory services like LDAP. Table 2 below outlines common LDAP operations and their JNDI equivalents. (For a detailed look at the JNDI specification, see Resources.)
Operation | What it does | JNDI equivalent |
Search | Search directory for matching directory entries | DirContext.search() |
Compare | Compare directory entry to a set of attributes | DirContext.search() |
Add | Add a new directory entry | DirContext.bind() ,
DirContext.createSubcontext() |
Modify | Modify a particular directory entry | DirContext.modifyAttributes() |
Delete | Delete a particular directory entry | Context.unbind() ,
Context.destroySubcontext() |
Rename | Rename or modify the DN | Context.rename() |
Bind | Start a session with an LDAP server | new InitialDirContext() |
Unbind | End a session with an LDAP server | Context.close() |
Abandon | Abandon an operation previously sent to the server | Context.close() ,
NamingEnumneration.close() |
Extended |
Extended operations command |
LdapContext.extendedOperation() |
Manipulate objects in the LDAP server
Let's cut to the chase and see how to manipulate objects in
the LDAP server. The standard LDAP operations include:
We'll examine each of these steps in the sections below, with examples.
Before executing the examples, you will need to install the LDAP server, the JNDI classes, and (unless you want to disable schema checking) the Java schema. You can find install information in the JNDI zip file's schema directory. Our examples use Netscape Directory Server 4.1 and JDK 2. (To install these packages, see Resources.)
Connect to the server
To connect to
the server, you must obtain a reference to an object that implements the
DirContext
interface. In most applications, this is done by using
an InitialDirContext
object that takes a Hashtable
as
an argument. The Hashtable
contains various entries, such as the
hostname, port, and JNDI service provider classes to use:
Hashtable env = new
Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL,
"ldap://localhost:389");
DirContext ctx = new
InitialDirContext(env);
Bind to the Server
Once connected,
the client may need to authenticate itself; this process is also known as
binding to the server. (Be aware that the word binding can
also refer to the act of adding something to the directory.)
In LDAP version 2, all clients had to authenticate while connecting, but version 3 defaults to anonymous and, if the default values are used, the connections are anonymous as well. LDAP servers maintain rights using access control lists (ACLs) that determine what particular access is available to an entry by an application. LDAP supports three different security types:
The client authenticates itself to the server by specifying values for
different environment variables in the Context
interface, as seen
below:
Hashtable env = new
Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL,
"ldap://localhost:389");
env.put(Context.SECURITY_AUTHENTICATION,"simple");
env.put(Context.SECURITY_PRINCIPAL,"cn=Directory
Manager"); // specify the
username
env.put(Context.SECURITY_CREDENTIALS,"password");
// specify the password
DirContext ctx = new
InitialDirContext(env);
Add new entries in the LDAP server: The options
The LDAP directory server can act as a repository for Java
objects. JNDI provides an object-oriented view of this directory, which means
that Java objects can be added to and retrieved from the directory without the
client needing to manage data representation issues.
Objects can be stored in three ways:
Let's take a look at each of these in more detail.
Store the Java objects themselves
If a class implements
the java.io.Serializable
interface, it can be serialized and
deserialized from storage media. If we need a simple name-object binding (as in
the RMI registry), then the Context.bind()
method can store the
object. But if we need the more powerful technique of associating attributes
with the stored object, we'd employ the DirConext.bind()
method
instead. Whichever method we use, the object's state is serialized and stored in
the server:
MyObject obj = new MyObject();
ctx.bind("cn=anobject", obj);
Once stored, we can retrieve the object by looking up its name in the directory:
MyObject obj = (MyObject)ctx.lookup("cn=anobject");
When an application serializes an object by writing it to an object stream, it records information that identifies the object's class in the serialized stream. However, the class's definition, which is contained in the classfile, is not itself recorded. The system that deserializes the object is responsible for determining how to locate and load the necessary class files.
Alternatively, the application can record the codebase with the
serialized object in the directory, either when the binding occurs or by
subsequently adding an attribute using
DirContext.modifyAttributes()
. (We'll examine this second technique
later in this article.) Any attribute can record the codebase as long as the
application reading back the object is aware of the attribute name. As another
option, we can employ the attribute "javaCodebase"
specified in the
LDAP schema for storing Java objects if schema checking is enabled on the
server.
The above example can be modified to supply a codebase attribute containing
the location of the MyObject
class definition:
// Create object to be bound
MyObject obj = new MyObject();
//
Perform bind and specify codebase
BasicAttribytes battr = new
BasicAttributes("javaCodebase","http://myserver.com/classes")
ctx.bind("cn=anobject",
obj, battr);
AddSeriaize.java
demonstrates how to add 10 instances of
java.util.Vector
, which implements
java.io.Serializable
; the result can be seen in Figure 1.
Figure 1. Screen shots of the server console after running the
ADDSerialize example |
Store a reference to the object
Instead of storing the
entire serialized state of an object, you can store a reference to that object
instead. JNDI's javax.naming.Reference
class records address
information about objects not directly bound to the directory service. The
reference to an object contains the following information:
javax.naming.RefAddr
objects that represents the
addresses
The javax.naming.RefAddr
abstract class, seen in Figure 2 below,
contains information indicating the ways in which you can contact the object
(e.g., via a location in memory, a lookup on another machine, etc.) or recreate
it with the same state. The class defines an association between content and
type. The content (an object) stores information required to rebuild the object
and the type (a string) identifies the purpose of the content.
Figure 2. The relation between a Reference, RefAddr, Type, and Content
|
RefAddr
also overrides the java.lang.Object.equals(Object
obj)
and java.lang.Object.hashcode()
methods to ensure that
two references are equal if the content and type are equal. RefAddr
has two concrete subclasses: javax.naming.StringRefAddr
, which
stores strings, and javax.naming.BinaryRefAddr
, which stores an
array of bytes. For example, a string reference address could be an IP, URL,
hostname, or something similar.
Consider the example of a referenceable Apartment
class. The ADDReference.java
example creates a few instances of Apartment
and stores them in the
server. What happens internally? Since the object is referenceable, a reference
is stored and not the serialized object. When the example tries to look up an
apartment belonging to styagi
, it gets a reference from the server
that contains information about the factory class needed, the apartment size,
and its location. It then requests that the factory create an
Apartment
object with the right size and location and return that
object. All this happens transparently to the user.
Context ctx = new
InitialContext(env);
ctx.bind("apartment=styagi,ou=JavaObjects,o=myserver.com",new
Apartment("studio","Mill
Complex"));
ctx.bind("apartment=mojoe,ou=JavaObjects,o=myserver.com", new
Apartment("2
room","Farm House
Apartments"));
ctx.bind("apartment=janedoe,ou=JavaObjects,o=myserver.com",new
Apartment("1
room","Pond
Side"));
ctx.bind("apartment=rogerp,ou=JavaObjects,o=myserver.com", new
Apartment("3
room","Mill
Complex"));
ctx.bind("apartment=jamesm,ou=JavaObjects,o=myserver.com",
new
Apartment("studio","Fox Hill
Apartments"));
ctx.bind("apartment=paulh,ou=JavaObjects,o=myserver.com", new
Apartment("duplex","Woodbridge"));
ctx.bind("apartment=vkevink,ou=JavaObjects,o=myserver.com",new
Apartment("1
room","Woodgate Apartments"));
Apartment
apt = (Apartment)ctx.lookup("apartment=styagi,ou=
JavaObjects,o=myserver.com");
System.out.println(apt);
The Apartment
class would look something like:
public class Apartment implements Referenceable{
private String
size;
public String location;
public Apartment(String size,String
location){
this.size=size;
this.location=location;
}
public
Reference getReference() throws NamingException{
String classname =
Apartment.class.getName();
StringRefAddr classref
=
new StringRefAddr("Apartment details", size+ ":" +location);
String
classfactoryname=ApartmentFactory.class.getName();
Reference ref = new
Reference(classname,classref,classfactoryname,null);
return
ref;
}
public String toString(){
return ("This apartment
is "+size+ " and is located at " +location);
}
}
The factory used to recreate the Apartment
objects, ApartmentFactory
,
is stored in the Reference
, as shown above. The
ApartmentFactory
implements the
javax.naming.spi.ObjectFactory
interface, which contains the
getObjectInstance()
method that returns a newly constructed object
based on the reference, as seen below:
public Object getObjectInstance(Object obj,Name name,
Context
ctx,Hashtable env)throws Exception{
Reference ref =
(Reference)obj;
RefAddr addr = ref.get("Apartment
details");
String temp = (String)addr.getContent();
int offset=
temp.indexOf(":");
String size
= temp.substring(0,offset);
String location =
temp.substring(offset+1);
return (new
Apartment(size,location));
}
The getObjectInstance()
method parses the components and tries
to reconstruct a reference that describes an Apartment
object. The
factory interprets the values of the reference, and it's free to ignore
nonrelevant parts and create a new object instead. The objects and properties
produced after ADDReference.java
runs can be seen in screen shots in Figures 3 and 3a.
Figure 3. Screen shot showing that objects were added to the server
|
:
Figure 3a. Screen shot of the object properties, highlighting the fact
that it is actually a reference |
Store information as attributes
The last technique to
store nonserializable and nonreferenceable objects involves storing attributes
rather than the object or a reference to the object. If the bound object
implements the DirContext
interface, the LDAP ADD
operation extracts and stores the object's attributes. This technique doesn't
store the actual object, but rather stores the attributes inside that object.
Consider the User
object:
public class User implements DirContext{
String
id;
String dn;
Attributes myAttrs = new
BasicAttributes(true);
Attribute oc = new
BasicAttribute("objectclass");
Attribute ouSet = new
BasicAttribute("ou");
public User(String dn,String uid, String
givenname,String sn,String
ou,
String
mail,String tel,String
fax){
this.dn=dn;
id
=
uid;
oc.add("inetOrgPerson");
oc.add("organizationalPerson");
oc.add("person");
oc.add("top");
ouSet.add("People");
ouSet.add(ou);
String
cn = givenname+"
"+sn;
myAttrs.put(oc);
myAttrs.put(ouSet);
myAttrs.put("uid",uid);
myAttrs.put("cn",cn);
myAttrs.put("sn",sn);
myAttrs.put("givenname",givenname);
myAttrs.put("mail",mail);
myAttrs.put("telephonenumber",tel);
myAttrs.put("facsimilietelephonenumber",fax")
}
public Attributes getAttributes(String name) throws NamingException
{
if (!
name.equals("")){
throw new
NameNotFoundException();
}
return
myAttrs;
}
public Attributes getAttributes(Name name) throws
NamingException {
return
getAttributes(name.toString());
}
public Attributes
getAttributes(String name, String[]
ids)
throws
NamingException {
if(!
name.equals(""))
throw new
NameNotFoundException();
Attributes
answer = new
BasicAttributes(true);
Attribute
target;
for (int i = 0; i <
ids.length; i++){
target =
myAttrs.get(ids[i]);
if
(target !=
null){
answer.put(target);
}
}
return
answer;
}
public Attributes getAttributes(Name name, String[]
ids)
throws
NamingException {
return
getAttributes(name.toString(), ids);
}
// other methods of
DirContext with blank implementations.
The User
object can be added simply by:
User usr = new
User("styagi","Sameer","Tyagi","ou=Accounting",
"sameertyagi@usa.net");
ctx.bind("uid=styagi,ou=People,o=myserver.com",
usr);
Let me emphasize again that this technique does not store the
User
object, but rather stores its attributes. AddAtributes.java
shows how you can add 10 Person
objects using attributes. Figures 4
and 4a show screen shots of the server and property sheet for the Moe Joe entry.
Figure 4. Screen shot showing that entries were added
|
Figure 4a. Screen shot showing attributes of an entry
|
Modify stored objects
Once we've
stored an object, we can add attributes to its entry -- a simple and strong
combination that allows us to return Java objects with a search for the objects'
attributes. The DirContext
class's modifyAttributes()
method can perform ADD
, REPLACE
, or
REMOVE
modifications. In the following example, we store an account
object and add an attribute lastlogin
with values of date, last
deposit, last withdrawal, and balance; we'll use the simpler, overloaded version
of the modifyAttributes()
method.
Attributes myAttrs = new BasicAttributes(true);
Attribute
oc = new
BasicAttribute("lastlogin");
oc.add("Monday
January 12 1999"); //date
oc.add("200");
//last deposit
oc.add("43"); //last
withdrawal
oc.add("7652");
balance
myAttrs.put(oc);
Account act =
new Account("styagi");
String
cn="cn=test,ou=JavaObjects,o=myserver.com";
ctx.bind(cn,
act);
ctx.modifyAttributes(cn,DirContext.ADD_ATTRIBUTE,myAttrs);
When an object attribute has multiple values, if the extra values are not
sent with a replacement value, they will be removed. This version of
modifyAttributes()
allows only a single atomic operation on the
object on one or more attributes. If several modifications to a stored object
entry are to be made, the ModificationItem
that takes an
Attribute
object and a modification type is used. Like all other
operations, an authenticated user with sufficient rights can perform these
operations. Modifications are applied in the order in which they appear in the
array, and either all of the modifications execute or none do. The example below
shows how to replace, add, or remove multiple attributes:
DirContext ctx = new InitialDirContext(env);ModificationItem[] mods = new
ModificationItem[3];Attribute mod0 = new
BasicAttribute("telephonenumber","860-888-8888");
Attribute mod1 = new
BasicAttribute("l", "Hartford");
mods[0] = new
ModificationItem(DirContext.REPLACE_ATTRIBUTE,mod0);
mods[1] = new
ModificationItem(DirContext.ADD_ATTRIBUTE,mod1);
mods[2] = new
ModificationItem(DirContext.REMOVE_ATTRIBUTE,mod1);
ctx.modifyAttributes("uid=styagi,ou=People,o=myserver.com",
mods);
MODObject.java
shows an example. However if modification involves renaming only the DN, then
the Context.rename()
method will suffice, as seen below:
Context.rename("uid=styagi,ou=People,o=myserver.com",
"uid=sameer,ou=People,o=myserver.com");
Figure 5 demonstrates the server's output from the
MODObject.java
example.
Figure 5. Property sheet for the vectorid-1 entry added in the
ADDSerialize example and modified by executing MODObject.java
|
Delete stored objects
A context
consists of a set of name-to-object bindings. The subcontext represents the
context under the context in the LDAP tree. We can delete the User
object by destroying the subcontext. The converse, however, is not true -- that
is, creating a subcontext with the DirContext.createSubcontext()
method doesn't add any objects. The Context.unbind()
method
produces the same output, but also works under a clustered naming system in
which a context from one naming system may be bound to a name in another. The
unbind()
method succeeds in removing this foreign context. (A
foreign context is by definition not a subcontext of the context in which it is
bound). Deleting objects works only for leaves in the tree; if tried on a node,
JNDI will throw a ContextNotEmptyException
.
To delete an object:
DirContext ctx = new
InitialDirContext(env);
ctx.destroySubcontext("uid=styagi, ou=People,
o=myserver.com");
DELObject.java
shows an example of deleting objects.
Search the server for an entry
LDAP
boasts feature-rich search facilities. The simplest level of search can use the
JNDI naming service to perform the following types of searches:
Context.lookup()
, which returns a stored object corresponding
to that DN in the server
Context.list()
, which returns the names bound and the class
names of the objects bound in the context
Context.listBindings()
, which returns the names bound and the
objects bound to them in the context The example DifferentSearches.java
looks up a DN and checks if the object returned is a Vector
, as
seen in Figure 6.
Figure 6. Output from DifferentSearches.java for a lookup operation
|
LDAP stores information in the tree and everything is specific to the node. Therefore, to search for an object in the LDAP server, you need to specify the node or the base from which to start -- the scope of the search. The scope defines exactly how much of the tree should be searched. There are three levels of scope, as outlined in Table 3 and Figures 7, 7a, and 7b below.
SUBTREE_SCOPE |
Starts at the base entry; searches the base entry and everything below it |
ONELEVEL_SCOPE |
Searches only the entries below the base entry |
OBJECT_SCOPE |
Searches only the base entry; useful if you need to get attributes/value pair of just one entry |
Figure 7. Searchbase: o=myserver.com Scope=LDAP_SCOPE_SUBTREE
|
Figure 7a. Searchbase: ou=people, o=myserver.com
Scope=LDAP_SCOPE_ONE_LEVEL |
Figure 7b. Searchbase :uid=styagi,ou=people,o=myserver.com Scope=
LDAP_SCOPE_ONE_LEVEL |
To perform the physical search, we invoke the search()
method on
the object that implements the DirContext
interface
(InitialDirContext
). The search result is
NamingEnumeration
, an enumeration of SearchResult
objects. At minimum, you need the search base and the filter in order to perform
a search, but other parameters can be specified to manage the results. The
SearchContols
object holds most additional parameters for the
search, including scope, as seen below:
SearchControls ctls = new
SearchControls();
setSearchScope(SearchControls.SUBTREE_SCOPE)
Search on attributes
The other type of search involves
searching the attributes of objects and entries for particular values. The
simplest form of search requires the set of attributes and the name of the
target context in which the search is to be performed. The following code shows
how to search the server for anything that has specified values for
uid
and email
. The result is a
NamingEnumeration
:
Attributes matchAttrs = new BasicAttributes(true);
matchAttrs.put(new
BasicAttribute("uid", "kevink"));
matchAttrs.put(new
BasicAttribute("mail","sameertyagi@usa.net"));
// Search for objects with
these matching attributes
NamingEnumeration answer =
ctx.search("ou=People,o=myserver.com",matchAttrs);
When the above code is executed, you get the result shown in Figure 8.
Figure 8. Output of differentSearches.java for a basic search.
|
Search with filters
LDAP works with search filters. A
search filter is a search query expressed in the form of a logical expression.
RFC 2254 describes the syntax of search filters that the
DirContext.search()
methods will accept (see Resources),
with the exception that Unicode characters are also allowed. Keep in mind that
the use of Unicode characters is preferable to the use of encoded UTF-8 octets.
The search filter syntax is a logical expression with a prefix notation (the
logical operator appears before its arguments). Table 4 summarizes the different
filter symbols and their meanings.
Symbol | Filter | Example | Example matches |
~ |
Approximate | (sn~=Tyagi) |
Tyagi or variations in spelling |
= |
Equality | (sn=Tyagi) |
Surname of Tyagi only |
> |
Greater than | (sn=Tyagi) |
Any surname that alphabetically follows Tyagi |
>= |
Greater than or equal to | (sn>=Tyagi) |
Any surname that includes or alphabetically follows Tyagi |
< |
Less than | (sn<Tyagi) |
Any surname that alphabetically precedes Tyagi |
<= |
Less than or equal to | (sn<=Tyagi) |
Any surname that includes or alphabetically precedes Tyagi |
=* |
Presence | (sn=*) |
All surnames (all entries with the
sn attribute) |
Substring | (sn=Tya*), (sn=*yag*),
(sn=Ty*g*) |
Any matching string, substring, or superstring that matches Tyagi | |
& |
And | (&(sn=Tyagi) (cn=Sameer
Tyagi)) |
Any entry that matches both surname of Tyagi and a common name of Sameer Tyagi |
| |
Or | (|(sn=Tyagi) (cn=Sameer Tyagi)) |
Any entry that matches either surname of Tyagi or a common name of Sameer Tyagi |
! |
Not | (!(sn=Tyagi)) |
Any entry other than that with a surname of Tyagi |
Each item in the filter is composed of an attribute identifier and either an
attribute value or symbols denoting the attribute value. The following search
filter specifies that the qualifying entries must have a userid
attribute with a value of styagi
and a jpeg photograph with bytes:
82 12 38 4e 23 e3
(binary data):
(&(uid=styagi)( jpegphoto=\82\12\38\4e\23\e3))
The following code demonstrates how to search using a filter and search controls:
SearchControls ctls = new SearchControls();
String filter =
"(&(cn=myappuser-1)( lastlogin=*))";
NamingEnumeration answer =
ctx.search("ou=People", filter, ctls);
The output of the above code can be seen in Figure 9.
Figure 9. DifferentSearches for a filtered search
|
Control search results
Let's now
turn our attention to controlling the results of the search.
Control attributes
By default, a search will return all
attributes associated with the result that satisfy the specified query. To
specify the desired attributes, create an array of attribute identifiers, then
invoke setReturningAttributes
on the search controls:
// Specify the ids of the attributes to return
String[] attrIDs =
{"cn", "lastlogin"};
SearchControls ctls = new
SearchControls();
ctls.setReturningAttributes(attrIDs);
The output of the above code can be seen in Figure 10.
Figure 10. Output of DifferentSearches for controlled attribute search
|
Control the number of results
By default, a search does
not have a limit on the number of results returned. If the search produces too
many results or consumes too many resources, the setCountLimit()
method can specify the number of results, although proper use of scope and
filters should be the preferred means to refine searches:
// Set search controls to limit count to 1
SearchControls ctls = new
SearchControls();
ctls.setCountLimit(1);
If the count limit is set and there are more results, the search may throw a
SizeLimitExceededException
.
Control time
If needed, the setTimeLimit()
method can set the maximum search completion time for the answers. This is a
useful technique when you don't want your program to block for an extended
period of time:
// Set search controls to limit time to 3 seconds
SearchControls ctls
= new SearchControls();
ctls.setTimeLimit(3000);
If the search exceeds its specified time limit before the search operation
can be completed, a javax.naming.TimeLimitExceededException
is
thrown. A time limit of 0
will make the program wait indefinitely
until the search is completed.
Return the Java objects in the result
It is important to
invoke the setReturningObjFlag(boolean on)
method, as it determines
whether the search returns the attributes stored in the server or the object
itself. It should be set to true
in order to get Java objects in
the NamingEnumeration
. If set to false
, the search is
faster, but it returns only the object name and class, not the object data:
SearchControls ctls = new SearchControls();
ctls. setReturningObjFlag
(true);
Control links
A link is an entry in a context
that references another entry, similar to a symbolic link in a filesystem.
Searches can either traverse links, looking for matches to a search, or ignore
them. The setDerefLinkFlag(boolean on)
method determines if this
happens or not:
SearchControls ctls = new SearchControls();
ctls.setDerefLinkFlag
(true);
DifferentSearches.java
shows an example of all the searches described above.
The COMPARE operation
In LDAP, a COMPARE
operation allows a client to specify a DN and an attribute/value pair (an
assertion provided with an entry) in the directory. The server determines if the
named entry has those name/value pairs as attributes, returning a boolean answer
indicating this. The COMPARE
operation allows the server to keep
certain attribute/value pairs secure by not exposing them to applications. The
server can compare the DN uid=styagi,ou=People,o=myserver.com
for
the attribute password=hotjava
, but it can't search for all users
who have hotjava
as the password. The comparison can be performed
in JNDI using a search with certain constraints:
OBJECT_SCOPE
SetReturningAttributes()
method should not be invoked, or
should be invoked with setReturningAttributes(new String[0])
As an example, DifferentSearches.java
examines the attribute's last login to see if it has a value of 200 for a DN of
cn=vectorid-i,ou=JavaObjects,o=myserver.com
:
SearchControls ctls = new SearchControls();
ctls. setReturningObjFlag
(true);
ctls.setReturningAttributes(new
String[0]);
ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
String
filter = "(lastlogin=200)";
NamingEnumeration answer
= ctx.search("cn=vectorid-1,ou=JavaObjects,o=myserver.com", filter,
ctls);
Since a search always returns enumeration, and there is no method to determine the number of results (except setting a counter) if the compare succeeds, the resulting enumeration will contain a single result with an empty name and no attributes.
Event handling
JNDI supports AWT
1.1-type event handling in the javax.naming.event
package. However,
you need to add the listeners to the objects after the objects are registered in
the JNDI tree if you want to make the tree event aware. The LDAP server must
support persistent search control for events to be propagated.
Table 5 shows the trapped NamingEvent
types.
|
An object's contents (e.g., its attributes) have changed or its binding have been replaced |
|
A new object has been added to the namespace |
|
An existing object has been removed from the namespace |
|
An existing object has been given another name |
For an object to be a listener, it must implement the
javax.naming.event.NamingListener
interface or either of its two
specialized subinterfaces: javax.naming.NamespaceChangeListner
or
javax.naming.event.ObjectChangeListener
. The
NamespaceChangeListener
registers interest in the DN and invokes
when objects are added, removed, or renamed to that DN. In contrast, an
ObjectChangeListener
, which is more single-object specific, invokes
when its attributes are modified or the object/object binding is replaced. MyNamingListener.java
and MyObjectListener.java
show how to implement a simple NamespaceChangeListener
and
ObjectChangeListener
, respectively.
Since we add the object in the JNDI tree to add a listener, that object first
has to be located. The EventContext
locates the object when the DN
or name of the object is known, whereas we employ EventDirContext
when we have to search for the object. The code from DifferentListeners.java
below shows how to add a NamespaceChange
listener to an
EventContext
:
Context inictx =getContext();
EventContext
ctx=(EventContext)(inictx.lookup("o=myserver.com"));
NamingListener objlsnr
=new
MyNamingListener();
ctx.addNamingListener("ou=JavaObjects",EventContext.SUBTREE_SCOPE,
objlsnr);
ctx.bind("cn=eventtest,ou=JavaObjects",new
Vector());
ctx.rename("cn=eventtest,ou=JavaObjects","cn=eventtestrenamed,ou=JavaObjects");
If the object's DN is not known, EventDirContext
can search for
the object starting from the ou=JavaObjects
in the DIT:
Context inictx =getDirContext();
EventDirContext
ctx=(EventDirContext)(inictx.lookup("o=myserver.com"));
NamingListener
naminglsnr= new MyNamingListener();
SearchControls ctls = new
SearchControls();
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
ctx.addNamingListener("ou=JavaObjects","(cn=*)",ctls,
naminglsnr);
// Test the
listener
ctx.unbind("cn=vectorid-9,ou=JavaObjects");
Just as in the case of a search, listeners have a scope (e.g.,
EventContext.ONELEVEL_SCOPE
) defining the part of the tree to which
they should listen. DifferentListeners.java
shows how to use a NamespaceChangeListener
. A screen shot of the
output can be seen in figure 11.
Figure 11. Screen shot of the console, showing how an event is trapped
|
Remove listeners
There are three ways to remove a
registered listener:
Context.close()
method invokes on the event source
and all registered listeners are removed
NamingExceptionThrown()
EventContext.removeNamingListener()
explicitly removes a
listener Conclusion
The Lightweight
Directory Access Protocol defines the ways in which clients should access data
from a server. As we've seen, LDAP is a powerful directory server, especially
when used in conjunction with Java's JNDI. In this article, we've defined
several important LDAP-related terms, looked at how to connect to an LDAP
server, examined how to store Java objects on the server, and detailed LDAP's
search features.
If the information presented here seems overwhelming, let me assure you that it's not. After you build a couple of applications, you'll appreciate LDAP's simplicity and usefulness.
About the author
Sameer Tyagi has been working on Java since the 1.02 days.
Besides developing Internet/intranet applications with server-side Java, he has
also run training sessions, workshops, and written articles. In other words,
when he's not doing Java, its because he's doing more Java.