Using Java Naming and Directory Interface (JNDI) to Develop Novell Directory Services-Enabled Applications
Articles and Tips: article
Software Engineer
Developer Support
01 Nov 1998
Thanks to John Sumsion, Steve Merrill, Ann Davis, and Selby Herrin for their assistance with this Developer Note.
Gives a brief introduction of JNDI. Also includes sample code for completing various NDS tasks; such as listing objects in an NDS container, reading attributes of an NDS object, creating and deleting an NDS object, modifying the NDS schema by adding custom attribute definitions and custom NDS classes, and searching for an NDS object.
- Introduction
- JNDI Architecture
- JNDI Application Programming Interface (JNDI API)
- Java Sample Code for Performing Basic NDS Tasks
- Conclusion
Introduction
This article shows you how to use the Java Naming and Directory Interface (JNDI) to develop NDS-enabled applications. Using Java as your programming language allows you to write a portable application that is also suitable for the Web environment. As compared to conventional C or C++ programming, using Java greatly simplifies the task of programming and speeds up the development time. The first section of this article gives a brief introduction of the Java Naming and Directory Interface (JNDI) developed by JavaSoft. The main section includes sample code for achieving different NDS tasks; such as listing objects in an NDS container, reading attributes of an NDS object, creating and deleting a new NDS object, modifying the NDS schema by adding custom attribute definitions and custom NDS classes, and searching for an NDS object.
JNDI Architecture
The Java Naming and Directory Interface (JNDI) is an API specified in the Java Programming language that provides an open, consistent interface for access to any naming or directory service implementation. As its name implies, JNDI is an interface. A Java interface is implicitly an abstract class which means there is no implementation. JNDI provides an abstraction layer for developers to perform naming operations without worrying about how different naming systems such as NDS, NIS, or the NetWare file system implement the operations.
This simplifies the job of application developers since the naming implementation is totally invisible to them. Application developers can easily write a generic program to browse different directories. However, if developers want to perform more name space specific functions with the objects returned from the naming operation, they will need to use name space specific classes provided by a specific vendor.
Figure 1: JNDI architecture.
JNDI consists of two parts: the JNDI API and the JNDI SPI.
JNDI Application Programming Interface (JNDI API)
The JNDI architecture diagram shows that Java applications sit on the top most layer, right above the JNDI API layer. These applications use the JNDI API to provide functions such as locating network resources, printing and database services, and accessing and modifying attributes of people and resources. Java application developers who want to write directory services enabled applications need to be very familiar with the JNDI API layer, which contains two packages: javax.naming and javax.naming.directory.
Before I introduce the concepts in JNDI, I would like to mention an important point. Since the same word "context" is being used by NDS and JNDI, it is important for you to differentiate the NDS context from the JNDI context. NDS and JNDI both use the term "context" as a key concept, but the meanings are different and totally independent from one another. A context as used in NDS is a reference point into an NDS tree which tells where an NDS object is located in the hierarchical tree structure. The Full Distinguished name (FDN) of an NDS object shows the context of the object. For instance, the name joe.engineering.novell refers to a user object in a container or context "engineering.novell". It tells where Joe is placed in the NDS tree. The name indicates that it is under the "engineering" organizational unit (OU) which is under the organization named "novell". The word "context" will be used frequently to refer to a JNDI context, not an NDS context. I will define the JNDI "Context" concept in the following section.
The JNDI API consists of two packages:
javax.naming
javax.naming.directory
javax.naming
The naming interface organizes information hierarchically and maps people-friendly names to addresses or objects that are machine-friendly, e.g. www.novell.com -> 137.65.2.5. It allows access to named objects through multiple name spaces.
The core interfaces and classes in this package are:
Context
Binding
Name
NameParser
A JNDI Context is defined as "an object whose state is a set of bindings with distinct atomic names." A Binding is defined as "the association of an atomic name with an object." A name-to-object binding contains the name of the bound object, the name of the object's class, and the object itself. A binding can be associated with a null object, and a context can contain zero bindings. All NDS objects, including the leaf objects are implemented as DirContext objects which are also contexts. These contexts, which represent NDS leaf objects, may contain zero bindings.
Every context has an associated naming convention. For example, Novell Directory Services names are arranged right to left and are by a period separated (joe.engineering.novell). The javax.naming package also provides a Name and a NameParser interface for developers to manipulate a name in a name space. Every name is relative to a context and every naming operation is performed on a context. A client obtains an initial context to provide a starting point for name resolution. The methods in the context class provide the basic functions such as looking up an object by its name, listing subordinates in a context, adding new named objects to a context or renaming a named object, etc.
javax.naming.directory
The directory interface provides access to directory objects which can contain attributes thereby providing attribute-based searching and schema support. The major interfaces in this package are DirContext, Attribute and Attributes.
DirContext. DirContext is a context which contains zero or more attributes and contains methods to examine and update its associated attributes, to search the directory, and to retrieve the schema information of the directory objects. Since it is also a context, it inherits all the methods in the context interface as well. All NDS objects and the files and directories of the NetWare file system are implemented as DirContexts. The schema defines the structure of a name space which contains information specifying what types of objects can be added to the directory and where they can be added, what mandatory and optional attributes an object can have, etc. The schema information is also implemented as a DirContext so all methods in the DirContext interface can be used to access the schema itself.
Attributes. Attributes represents a collection of attributes associated with a DirContext object.
Attribute. Attribute is an attribute associated with a named object which contains an identifier and zero or more unique values. The definition of the attribute and the syntax of its value(s) are defined in the schema if the underlying directory service supports schemas.
JNDI Service Provider Interface (SPI)
The JNDI SPI is not for application developers, but for developers who write a service provider implementation for a naming system such as Novell Directory Service (NDS). Figure 1 shows that the SPI layer sits below the Application Interface layer and right above those red boxes which represent examples of different existing naming and directory implementations such as DNS, NDS, NIS (YP), X.500, LDAP Servers, RMI, CORBA, etc. A JNDI service provider is actually a "context implementation" which provides a class that implements the "Context" or the "DirContext" interface in the JNDI API as explained above. By implementing the interface and providing the actual methods of the interface, the vendors of different naming and directory service providers can expose their services to JNDI-enabled applications.
JNDI has a concept of "federation" which means a context in one naming system can be bound to a context in another naming system. It is the decision of the service provider vendor whether "federation" is supported. If it is supported, name resolution across multiple name spaces is handled in the service provider and is completely transparent to the application programmer. The implementation of federation enables a single application to access data across multiple name spaces seamlessly because different service providers cooperate with each other to complete JNDI operations. Novell fully supports the notion of "federation" and provides multiple service providers for different NetWare name spaces which enable NetWare application programmers to navigate through the global Novell name space to access different NetWare services.
In the Novell Developer Kit, Novell shipped the following service providers: Novell Directory Services (NDS), NetWare file system, Bindery, NCP Extensions, Queue Management System (QMS), Lightweight Directory Access Protocol (LDAP), NetWare Server, and the top level NetWare name system which includes all Novell naming systems.
In addition to the brief explanation of JNDI above, to fully understand and to start serious programming using JNDI, you should read the specification documents provided by JavaSoft at http://www.javasoft.com/products/jndi.
Java Sample Code for Performing Basic NDS Tasks
JNDIListObjs - List NDS Objects in a Container
1 import java.util.Hashtable; 2 import javax.naming.*; 3 4 class JNDIListObjs 5 { 6 7 public static void main(String[] args) 8 { 9 10 String contextName = ""; 11 12 if (args.length > 0) 13 contextName = args[0]; 14 15 Hashtable env = new Hashtable(); 16 17 env.put(Context.INITIAL_CONTEXT_FACTORY, 18 "com.novell.service.nw.NetWareInitialContextFactory"); 19 20 try 21 { 22 Context ctx = new InitialContext(env); 23 ctx = (Context) ctx.lookup(contextName); 24 NamingEnumeration nameClassEnum = ctx.list(""); 25 26 while (nameClassEnum.hasMore()) 27 { 28 NameClassPair ncPair = (NameClassPair)nameClassEnum.next(); 29 System.out.println(ncPair.getName()); 30 } 31 } // end try 32 catch (NamingException e) 33 { 34 System.err.println("JNDIListObjs failed."); 35 e.printStackTrace(); 36 } 37 } // end main 38 } // end class JNDIListObjs
Code Analysis of JNDIListObjs
You can experience the power of JNDI by examining this small sample of only thirty-eight lines of code. JNDIListObjs will list objects in any container of any naming system that has a JNDI service provider. This sample is written in a generic way. It does not use any Novell specific classes. Because of this, it can list any Java object that has implemented the Context interface of JNDI. The initialization of the initial context determines which name space the program will access.
1 import java.util.Hashtable;
This import statement is included because of the need to use the Hashtable class to initialize the environment property for the initial context. One of the constructors of the InitialContext class in the javax.naming package takes an environment contained in a Hashtable or one of its subclasses. JNDI includes system properties for applications to define the environment in which naming and directory services are accessed. Individual directory service providers decide how to map the environment properties that are suitable for their services. For a list of JNDI system properties with a detailed explanation, please refer to the JNDI API specification provided by JavaSoft (Section 5.3 and appendix A).
2 import javax.naming.*;
The javax.naming package needs to be imported because the program uses several of its classes and interfaces: Context, InitialContext and NamingEnumeration.
10 String contextName = ""; 12 if (args.length > 0) 13 contextName = args[0];
The program initializes the context variable to an empty string. If no context argument is entered at the command line by the user, an empty string will be supplied as a JNDI context. Passing an empty name to any JNDI operation means that the operation will be performed on the current context.
15 Hashtable env = new Hashtable();
This creates an instance of Hashtable to specify the initial context environment property.
17 env.put(Context.INITIAL_CONTEXT_FACTORY, 18 "com.novell.service.nw.NetWareInitialContextFactory");
As mentioned before, all naming operations are relative to a context. A JNDI name is always relative to a context and therefore a starting context has to be obtained for all name resolution operations. The application should set the initial context to a useful starting context which contains bindings that hook up the client to shared contexts from one or more naming systems. The default policy uses the system property "java.naming.factory.initial" to locate the initial context factory at runtime. If an environment property is passed to the InitialContext constructor as in the sample code, it will override the system property. The Context interface defines a constant called INITIAL_CONTEXT_FACTORY to use for this environment key name. InitialContextFactory is an interface for creating an initial context instance. A Hashtable essentially contains a key and its value, the "put" method associates the key "java.naming.factory.initial" with the value of "com.novell.service.nw.NetWareInitialContextFactory", which is the full qualified class name of the InitialContextFactory class contained in the NetWare JNDI service provider. The context is set at the Netware name space, which is the top level of all Novell name spaces. The application can navigate through all subordinate Novell name spaces such as NDS, NetWare file system, bindery, NCP extensions, etc.
22 Context ctx = new InitialContext(env);
The program obtains an initial context by invoking the constructor of the InitialContext class which selects an actual initial context implementation from a service provider.
23 ctx = (Context) ctx.lookup(contextName);
With the context, the application can invoke context methods. The lookup() method is the most important context operation and actually the only required operation. It is like a white page lookup in the phone book where you try to retrieve the record by specifying the name. The lookup() method takes a JNDI Name object or a string which contains a JNDI Name. The argument "contextName" passed into the lookup() method is either an empty string as default in this program or a user-specified context name at the command line. If the context name is an empty string, the lookup() method return the object pointed to by the current Context. Since lookup() essentially returns any kind of Java object, the returned Java object is explicitly cast into a Context object so that we can use this new context to list its bindings.
24 NamingEnumeration nameClassEnum = ctx.list("");
The list() method enumerates the names and the class names of the objects bound to the current context. The "ctx" variable now contains the handle to the user-specified context. The list() method takes take an empty string as its parameter because the naming operation should be performed on the current context.
26 while (nameClassEnum.hasMore()) 27 { 28 NameClassPair ncPair = (NameClassPair)nameClassEnum.next(); 29 System.out.println(ncPair.getName()); 30 }
The above while loop iterates through the enumeration until there are no more elements and casts each element in the enumeration to a NameClassPair object. Then we call getName() to get the name of the object and output the name of each entry.
35 e.printStackTrace();
A try block is necessary to enclose all the code that calls the JNDI naming methods because NamingException could be thrown. The Exception class provides a printStackTrace() method which will display the stack trace for debugging. A list of the Novell error codes can be found at the SDK documentation under http://developer.novell.com/sdk/htmldoc/nwhtm/index.htm. If the error code is not included in the stack trace, you can use the dumpException(e) method in the Debug class to get the error code and more debug information.
Run Time Experiment
Following are some suggestions of different command line options you can supply to experiment with the list sample code so that you can understand how a single interface can access different naming systems such as the file system on a NetWare server, the Novell Directory Service, or all the NCP extensions that are loaded on a NetWare server, etc.
java JNDIListObjs (Supply no argument) Output:Trees Servers
If you do not supply any argument, the context is initialized to an empty string which means that the list operation will be relative to the current context. The current context will be the initial context of the top level NetWare name space. The NetWare name space contains two static bindings which are Trees and Servers.
java JNDIListObjs Trees
If you specify "Trees" as the context name, the program will list all the NDS trees in the network.
java JNDIListObjs Servers
If you specify "Servers" as the context name, it will list all the NetWare servers in the network.
java JNDIListObjs Trees/JAVALAB411_TREE Output:Novell
If you specified "Trees/javalab411_tree" where javalab411_tree is an NDS tree name, federation will occur. The NetWare service provider will cooperate with the NDS service provider for this name resolution across multiple name spaces. All this cooperation happens underneath and is transparent to the application. The output is Novell because Novell is the organization name which sits at first level of the NDS tree.
java JNDIListObjs Trees/JAVALAB411_TREE/ou1.Novell
This will list all the subordinates under ou1.novell container. Trees/javalab411_tree/ou1.novell is a composite name which spans two name spaces. The portion "ou1.Novell" is in the format of a NDS fully distinguished name.
java JNDIListObjs Trees/JAVALAB411_TREE/ou1.Novell
With "Servers/javalab411" as the context name, the program will resolve to the Server name space which contains three static bindings: FileSystem, Bindery, NCPExtensions.
java JNDIListObjs Servers/JAVALAB411 (Javalab411 is a 4.11 server name)
If the user specifies "Servers/javalab411/FileSystem" as the context name, the program will federate into the file system name space of the Javalab411 server.
java JNDIListObjs Servers/JAVALAB411/FileSystem
This will federate into the bindery name space of the Javalab411 server.
java JNDIListObjs Servers/JAVALAB411/Bindery
This will federate into the NCP Extensions name space of the Javalab411 server where all the loaded NCP extensions can be listed.
JNDIGetattrs - Read Selected Attributes of an NDS Object
1 import java.util.Hashtable; 2 import javax.naming.*; 3 import javax.naming.directory.*; 4 import com.novell.service.nds.*; 5 6 class JNDIGetattrs 7 { 8 9 public static void main(String[] args) 10 { 11 12 if (args.length < 2) 13 { 14 System.out.println("Usage: JNDIGetattrs entryDN hostName"); 15 return; 16 } 17 18 String entryDN = args[0]; 19 String hostName = args[1]; 20 21 Hashtable env = new Hashtable(); 22 23 env.put(Context.INITIAL_CONTEXT_FACTORY, 24 "com.novell.service.nds.naming.NdsInitialContextFactory"); 25 env.put(Context.PROVIDER_URL, hostName); 26 27 try 28 { 29 DirContext ctx = new InitialDirContext(env); 30 31 String desiredAttrs[] = {"Full Name", "Surname", "Telephone Number"}; 32 Attributes result = ctx.getAttributes(entryDN, desiredAttrs); 33 if (result.size() == 0) 34 { 35 System.out.println(entryDN + "has none of these attributes"); 36 } 37 else 38 { 39 NamingEnumeration attrEnum = result.getAll(); 40 while(attrEnum.hasMore()) 41 { 42 Attribute attr = (Attribute) attrEnum.next(); 43 System.out.println("\nAttribute ID: " + attr.getID()); 44 45 NamingEnumeration attrValueEnum = attr.getAll(); 46 47 while(attrValueEnum.hasMore()) 48 { 49 NdsAttributeValue value = (NdsAttributeValue) attrValueEnum.next(); 50 System.out.println("Attribute Value: " + value); 51 52 if ( value instanceof NdsCaseIgnoreString) 53 { 54 NdsCaseIgnoreString strValue = (NdsCaseIgnoreString) value; 55 System.out.println(strValue.getCaseIgnoreSt ring()); 56 } 57 } 58 } // end outer while loop 59 } // end else 60 } // end try 61 catch (NamingException e) 62 { 63 System.out.println("JNDIGetattrs failed."); 64 e.printStackTrace(); 65 } 66 } // end main 67 } // end class JNDIGetattrs
Code Analysis of JNDIGetattrs
1 import java.util.Hashtable;
Again, the Hashtable is used to initialize the context environment properties.
2 import javax.naming.*;
javax.naming is imported for the NamingEnumeration interface.
3 import javax.naming.directory.*;
This package is imported because the sample uses DirContextand Attributes. DirContextis used instead of Contextbecause we are dealing with a JNDI context that supports access to attributes.
18 String entryDN = args[0]; 19 String hostName = args[1];
This program takes two arguments: the full distinguished name of the NDS object that we want to examine and the hostName, which is the NDS tree which contains the NDS object.
23 env.put(Context.INITIAL_CONTEXT_FACTORY, 24 "com.novell.service.nds.naming.NdsInitialContextFactory");
Then the program creates and initializes JNDI environment properties: the initial context factory and the provider URL. Instead of setting the initial context factory to the top level NetWare context factory as in the previous sample, this sample sets the value of the initial context factory key to the class name of the InitialContextFactoryclass implemented by the NDS service provider. This means that the starting context for all name resolution operations points at the NDS name space directly.
25 env.put(Context.PROVIDER_URL, hostName);
The name of the environment property for specifying the initial location within a provider's name space is "java.naming.provider.url". This string is also represented by the PROVIDER_URLconstant in the Context interface. If this system property is not initialized and the environment property is not set, the service provider will use its own configuration and discovery protocols. The NDS service provider uses provider URL as the NDS tree name. So the provider URL should be initialized to the NDS tree which contains the NDS object that we are going to read.
29 DirContext ctx = new InitialDirContext(env);
Get a DirContextby calling the constructor of InitialDirContextand pass in the environment properties that we just initialized.
31 String desiredAttrs[] = {"Full Name", "Surname", "Telephone Number"};
Create an array of the strings containing the attribute IDs of the attributes we want to read from the object.
32 Attributes result = ctx.getAttributes(entryDN, desiredAttrs);
Call the getAttributes()method to get the selected attributes of the specified NDS object. We keep a reference of type Attributes which stores a collection of Attribute.
39 NamingEnumeration attrEnum = result.getAll();
Invoke the getAll() method to get an enumeration of all the attributes that are contained in the attribute set.
40 while(attrEnum.hasMore()) 41 { 42 Attribute attr = (Attribute) attrEnum.next(); 43 System.out.println("\nAttribute ID: " + attr.getID());
The outer while loop iterates through each attribute in the attribute collection, retrieves and prints the attribute ID.
45 NamingEnumeration attrValueEnum = attr.getAll();
The getAll()method in Attribute retrieves an enumeration of the attribute's values.
47 while(attrValueEnum.hasMore()) 48 { 49 NdsAttributeValue value = (NdsAttributeValue) attrValueEnum.next();
This inner while loop iterates through each value of the attribute and cast it into a generic NdsAttributeValueobject.
50 System.out.println("Attribute Value: " + value);
Since the values of NDS attributes can be of different syntaxes, just invoking printlnon "value" will not always output the value in the proper format, you will see non-printable character for attributes that do not contain ASCII data. The following lines show you how to print the data in the proper way.
52 if ( value instanceof NdsCaseIgnoreString) 53 { 54 NdsCaseIgnoreString strValue = (NdsCaseIgnoreString) value; 55 System.out.println(strValue.getCaseIgnoreString()); 56 }
The proper way to get nice output is to test the value against specific NDS syntaxes to decide how to print the value according to its data format. There are twenty-seven different NDS syntaxes. The NDS service provider provides a class for each syntax and there are methods inside these classes for the Java application to convert the data to a printable format. For a value with the "Case Ignore String" syntax, the value should be cast into the NdsCaseIgnoreStringobject and its value printed using the getCaseIgnoreString()method.
If you want to read all the attributes of an NDS object, it is even simpler than this sample. All you need to do is to change one line of this sample. You invoke getAttributeswithout passing in the desiredAttrsarray.
32 Attributes result = ctx.getAttributes(entryDN);
JNDICreateSubctx - Create and Delete NDS Objects
This sample code uses JNDI to create an organizational unit (OU) and a user object under the newly created OU. It creates the user with a set of single and multi-valued attributes. If the name for the new OU is not supplied on the command line, it will use a default OU with the name "ou1.novell". An exception will be thrown if your NDS tree does not contain an organizational unit named "ou1.novell". So you should probably supply your OU name at the command line and also modify the organization name to match a name in your tree before running it. To run the sample, type: java JNDICreateSubctx hostName newUser [newou]
1 import javax.naming.*; 2 import javax.naming.directory.*; 3 import java.util.*; 4 import com.novell.service.nds.*; 5 6 class JNDICreateSubctx 7 { 8 9 public static void main(String[] args) 10 { 11 if (args.length < 2) 12 { 13 System.out.println(args.length); 14 System.out.println("Usage: JNDICreateSubctx treeName newuser [newou]"); 15 System.exit(1); 16 } 17 18 String treeName = args[0]; 19 String ouName = "ou=ou1"; 20 String oName = ".o=novell"; 21 22 if (args.length > 2) 23 ouName = "ou="+ args[2]; 24 25 String ouDN = ouName + oName; 26 String user = "cn="+args[1]; 27 String userDN = user + "." + ouDN; 28 29 DirContext dirCtx; 30 31 Hashtable env = new Hashtable(); 32 33 env.put(Context.INITIAL_CONTEXT_FACTORY, "com.novell.service.nds.naming.NdsInitialContextFactory"); 34 35 env.put(Context.PROVIDER_URL, treeName); 36 37 try 38 { 39 40 dirCtx = new InitialDirContext(env); 41 42 if (args.length > 2) // only create a new ou if the second argument is supplied 43 { 44 45 Attributes ouAttrs = new BasicAttributes(); 46 47 ouAttrs.put("Object Class", // attribute ID which tells the class of the object 48 new NdsClassName("Organizational Unit")); // the value of the Object Class attribute 49 50 ouAttrs.put("OU", // attribute ID for the naming attribute 51 new NdsCaseIgnoreString(ouName)); // the name of the organizational unit. 52 53 dirCtx.createSubcontext(ouDN, // the full distinguished name of the OU 54 ouAttrs); // the attribute set which contains the two manadatory attributes 55 System.out.println(ouDN + " created"); 55 System.out.println(ouDN + " created"); 56 } 57 58 Attributes createAttrs = new BasicAttributes(); 59 60 // mandatory attributes 61 62 createAttrs.put("Common Name", new NdsCaseIgnoreString(userDN)); 63 createAttrs.put("Object Class",new NdsClassName("user")); 64 createAttrs.put("Surname", new NdsCaseIgnoreString("test")); 65 66 // Create optional attributes 67 68 // Boolean attributes 69 createAttrs.put("Allow Unlimited Credit", new NdsBoolean(true)); 70 createAttrs.put("Locked By Intruder", new NdsBoolean(false)); 71 72 // Case Ignore List 73 String[] lang = { "English", "French", "German", "Spanish" }; 74 createAttrs.put("Language", new NdsCaseIgnoreList(lang)); 75 76 // Case Ignore String 77 createAttrs.put("Description", new NdsCaseIgnoreString("a Netware user")); 78 79 // do a multi-valued add 80 Attribute title = new BasicAttribute("Title", new NdsCaseIgnoreString("CEO")); 81 title.add(new NdsCaseIgnoreString("custodian")); 82 title.add(new NdsCaseIgnoreString("engineer")); 83 createAttrs.put(title); 84 85 // Counter 86 createAttrs.put("Login Intruder Attempts", new NdsCounter(4)); 87 88 // EMail Address 89 createAttrs.put("EMail Address", new NdsEMailAddress(0, test@novell.com")); 90 91 // Facsimile Telephone Number 92 byte[] faxData = new byte[0]; 93 createAttrs.put("Facsimile Telephone Number", new NdsFAXNumber("123-4567", 0, faxData)); 94 95 // Integer 96 createAttrs.put("Login Maximum Simultaneous", new NdsInteger(40)); 97 98 // Net Address 99 byte[] address = {0x01,0x01,0x05,0x50,0x00,(byte)0xA0, (byte)0xC9,0x31,(byte)0x87,0x61,0x00,0x00 }; 100 createAttrs.put("Network Address", new NdsNetAddress(0,address)); 101 102 // Object ACL 103 createAttrs.put("ACL", new NdsObjectACL("Network Address", "[public]", 2)); 104 105 // Path 106 createAttrs.put("Home Directory", new NdsPath(0, "CN=mmak_411a_VOL1.O=novell", "SYSTEM")); 107 108 // Postal Address 109 String[] postal = {"abc","def","ghi","jkl","mno","pqr" }; 110 createAttrs.put("Postal Address", new NdsPostalAddress(postal)); 111 112 // Telephone Number 113 createAttrs.put("Telephone Number", new NdsTelephoneNumber("(123)456-7899")); 114 115 dirCtx.createSubcontext(userDN, createAttrs); 116 System.out.println(userDN + " created"); 117 } 118 catch (NamingException e) 119 { 120 e.printStackTrace(); 121 } 122 123 // dirCtx.destroySubcontext(userDN); 124 // dirCtx.destroySubcontext(ouName); 125 126 } // end main 127 } // end JNDICreateSubctx
Code Analysis of JNDICreateSubctx
4 import com.novell.service.nds.*;
This import statement is needed because of the use of different NDS syntax classes.
Lines 10 - 28. This section is to convert the user-specified relative distinguished name (RDN) into a full distinguished name (FDN). An NDS FDN is from right to left and period separated, so if at the command line, the user name bob and the organization unit name newou is supplied, the FDN for the user is formatted into bob.newou.novell . The sample hard-codes the organization name to be novell, so you will need to modify this to the name that you have in your NDS tree.
33 env.put(Context.INITIAL_CONTEXT_FACTORY, "com.novell.service.nds.naming.NdsInitialContextFactory");
Set the initial context to the NDS name space directly by initializing the JNDI environment property to the class name of the InitialContextFractory implemented by the NDS service provider.
35 env.put ( Context.PROVIDER_URL, treeName);
The NDS service provider requires that a tree name be specified as the host name.
40 dirCtx = new InitialDirContext(env);
Obtain a starting DirContext by creating an InitialDirContextinstance.
Lines 42 - 56. If the name of the OU is supplied, this section will be executed to create a new organizational unit.
45 Attributes ouAttrs = new BasicAttributes();
Create an Attributes object to specify a set of attributes to be created in the new OU object.
47 ouAttrs.put("Object Class", // attribute ID which tells the class of the object 48 new NdsClassName("Organizational Unit")); // the value of Object Class attribute
Every NDS object to be created needs to specify what kind of object it is. So "Object Class" is always a mandatory attribute when creating an object. Since we are going to create an organizational unit object, the value of the Object Class attribute is "Organizational Unit". The put() method in BasicAttributes adds an attribute to the attribute set.
50 ouAttrs.put("OU", // attribute ID for the naming attribute 51 new NdsCaseIgnoreString(ouName)); // the name of the organizational unit.
Another mandatory attribute for an OU object is the "OU" attribute. This is also the naming attribute which is created to hold the name of the NDS object.
53 dirCtx.createSubcontext(ouDN, // the full distinguished name of the OU 54 ouAttrs); // the attribute set which contains the two manadatory attributes
Invoke createSubcontext()to create the NDS OU object. This method takes two arguments: the name of the entry and the attribute set which contains the attributes to be used to create the entry.
Lines 58 - 116. This section creates the NDS user object under the newly created OU or the default OU.
58 Attributes createAttrs = new BasicAttributes();
This is same as the previous OU creation. An Attributes object is created to contain all the attributes that are going to be used to create the user object.
Lines 62 - 64. Create mandatory attributes for the NDS user. The mandatory attributes are Object Class, Surname, and Common Name. The information about what syntax should be used for each attribute is stored in the NDS schema documentation. The Novell Java Class Library includes classes for all the NDS syntaxes for application developers to conveniently construct their values using the required syntaxes.
Lines 66 - 113. Create optional attributes for the NDS user.
106 createAttrs.put("Home Directory", new NdsPath(0, "CN=mmak_411a_VOL1.O=novell", "SYSTEM"));
The home directory attribute stores a file system path that will be used to create the home directory. You will have to modify the value of the home directory attribute because you will have a different volume and path name in your NDS tree.
Lines 80 - 84. Show how to create a multiple-values attribute
80 Attribute title = new BasicAttribute("Title", new NdsCaseIgnoreString("CEO")); 81 title.add(new NdsCaseIgnoreString("custodian")); 82 title.add(new NdsCaseIgnoreString("engineer")); 83 createAttrs.put(title);
First, a BasicAttributeobject is created to store a single attribute. The add()method is invoked to add a value into an attribute. Then "title" is put into the attribute set.
115 dirCtx.createSubcontext(userDN, createAttrs)
Finally, after the attribute set is initialized properly with the all the values, createSubcontext() is invoked to create the new user. If this operation fails, it will throw a NamingException. Since we are trying to create many attributes for the user, if there is any mistake in any one attribute, it will cause a failure in the final creation of the user object. For an explanation of any error codes returned in the stack trace, refer to the SDK return value list at http://developer.novell.com/sdk/htmldoc/nwhtm/index.htm.
123 // dirCtx.destroySubcontext(userDN); 124 // dirCtx.destroySubcontext(ouName);
These two lines will delete the NDS objects that the sample has just created. They are commented out so that you can verify whether the objects have been created successfully.
JNDISearch - Searching for an NDS Object
1 import java.util.Hashtable; 2 import javax.naming.*; 3 import javax.naming.directory.*; 4 5 class JNDISearch 6 { 7 static String defaultFilter = "(Object Class = User)"; 8 static String defaultHost = "my411-tree"; 9 10 static String contextStr = ""; 11 12 public static void main(String[] args) 13 { 14 String filterExpr = null; 15 String hostName = defaultHost; 16 17 if (args.length == 0) 18 { 19 System.out.println("Optional usage: java JNDISearch [filter] [context] [hostname]"); 20 System.out.println("default host: "+defaultHost + 21 "\ndefault filter :"+defaultFilter + " are used"); 22 } 23 24 if (args.length > 0) // has at least one argument 25 filterExpr = args[0]; 26 if (args.length > 1) // has at least two arguments 27 contextStr = args[1]; 28 if (args.length > 2) 29 hostName = args[2]; 30 31 Hashtable env = new Hashtable(); 32 33 env.put(Context.INITIAL_CONTEXT_FACTORY, 34 "com.novell.service.nds.naming.NdsInitialContextFactory"); 35 36 env.put(Context.PROVIDER_URL, hostName); 37 38 39 try { 40 41 DirContext dirCtx = new InitialDirContext(env); 42 43 SearchControls constraints = new SearchControls(); 44 45 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 46 47 if (filterExpr == null) 48 filterExpr = defaultFilter; 49 50 51 NamingEnumeration results = 52 dirCtx.search(contextStr, // dirContext for the search operation to be performed on 53 filterExpr, // the filter expression 54 constraints); // Search controls which specify constraints eg. search scope 55 56 57 while (results != null && results.hasMore()) 58 { 59 SearchResult si = (SearchResult) results.next(); 60 System.out.println("name: " + si.getName()); 61 } // end while loop 62 } // end try 63 catch (NamingException e) 64 { 65 System.err.println("JNDISearch failed."); 66 e.printStackTrace(); 67 } 68 } // end main 69 } // end class JNDISearch
Code Analysis of JNDISearch
7 static String defaultFilter = "(Object Class = User)"; 8 static String defaultHost = "my411-tree"; 10 static String contextStr = "";
The above lines initialize some default values for basic configurations. They set the default filter expression to search for objects of the user class, the default host to be a NDS tree name and the contextStr variable to an empty string which means the current context.
Line 24-29. This program takes three optional arguments: the filter expression, the context to operate on, and the hostname or NDS tree name. If they are not supplied at the command line, default values will be used.
Line 31-36. Initialize JNDI environment properties. Initialize the initial context to the NDS name space and set the host name to the NDS tree that we want to search.
41 DirContext dirCtx = new InitialDirContext(env);
Get a DirContext by constructing an instance of InitialDirContext.
43 SearchControls constraints = new SearchControls();
Create an instance of SearchControlsto specify constraints for the search operation. This SearchControlsclass allows you to specify search constraints, search scope and search results. The serialized form of a SearchControlsobject consists of the search scope (an int) the time limit (an int), the "dereference links" flag (a boolean), the "return objects" flag (a boolean), the count limit (an int), and a String array naming the attributes to return. There are two constructors for this class. You can pass in the value of all five constraints in constructor or use setter methods to initialize those constraints. In this sample, the no parameter constructor is used, so the defaults apply. The defaults are: search one level; no maximum return limit for search results; no time limit for search; return all attributes associated with objects that satisfy the search filter; do not return named object (return only name and class); do not dereference links during search.
45 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
Set the scope of the search to search the entire subtree rooted at the named object.
47 if (filterExpr == null) 48 filterExpr = defaultFilter;
If the user does not pass in a filter expression, the default one is used, which is all objects that belong to the user class. The javadoc says that the format and interpretation of `filter' follows the specification in RFC 2254. The filter expression uses a prefix notation which put the operator as a prefix. The filter expression is enclosed by parentheses. If you want to search for entries with the surname attribute "mak" and the CN attribute "mary", the expression should be "("(surname=mak)(cn=mary))".
Line 51-55. Invoke the search.
Line 57-61. The while loop iterates through the returned NamingEnumeration and casts each element into a SearchResultobject. SearchResult extends Binding which extends NameClassPair. It calls getName() to retrieve the object name to be printed. To run this sample:
java JNDISearch ( if no argument supplied, default values will be used) java JNDISearch [filter expression] [context] [hostname] e.g.: java JNDISearch "(object class = printer)" novell mary-tree java JNDISearch "(&(surname = mak)(cn=mary))" ou1.novell mary-tree
JNDIExtSchema.java - Extend the NDS Schema (Add Custom Attribute and Class)
1 import java.util.*; 2 import javax.naming.*; 3 import javax.naming.directory.*; 4 import com.novell.service.nds.*; 5 import java.io.*; 6 7 public class JNDIExtSchema 8 { 9 public static void main(String[] args) 10 { 11 String treeName = "mary-tree"; 12 String context = "novell"; 13 String newObject = "MyObject"; 14 String newAttr = "MyAttribute"; 15 String newClass = "MyClass"; 16 String attrValue = "MyValue"; 17 try 18 { 19 // WARNING: do not run this test code on a production tree. 20 // This program will add attribute and class to your schema. 21 22 String warning = "Warning : Do not run this program on your production tree. This program will modify your schema."; 23 warning = warning + "It will add a custom attribute and a custom class to your schema, and create an object of the newly "; 24 warning = warning + " created class with the custom attribute."; 25 warning = warning + " Are are sure you want to proceed ? "; 26 27 System.out.println(warning); 28 29 BufferedReader in = new BufferedReader( new InputStreamReader(System.in)); 30 String ans = in.readLine(); 31 if (!ans.equalsIgnoreCase("y") ) 32 System.exit(1); 33 34 if (args.length > 0) 35 newClass = args[0]; 36 37 if (args.length > 1) 38 newAttr = args[1]; 39 40 if (args.length > 2) 41 context = args[2]; 42 43 if (args.length > 3) 44 treeName = args[3]; 45 46 Hashtable env = new Hashtable(); 47 48 env.put(Context.INITIAL_CONTEXT_FACTORY, "com.novell.service.nds.naming.NdsInitialContextFactory"); 49 env.put(Con 50 51 DirContext ctx = (DirContext) new InitialContext(env).lookup(context); 52 DirContext schemaRoot = ctx.getSchema(""); 53 54 Attributes attrs; 55 56 // first create the new attribute type with the syntax of a case exact string 57 58 attrs = new BasicAttributes(); 59 attrs.put("Syntax ID", new NdsInteger(NdsSyntaxId.CASE_EXACT_STRING_ID)); 60 schemaRoot.createSubcontext(newAttr + ".attributes", attrs); 61 System.out.println(newAttr + " custom attribute created\n"); 62 63 // Create a new class which inherits from "Top" class. 64 65 attrs = new BasicAttributes(); 66 67 attrs.put(SchemaClass.ATTR_OBJECT_SUPER_CLASSES, 68 new NdsDistinguishedName("Top")); 69 70 // flag the class as an effective and Container class 71 72 attrs.put(SchemaClass.ATTR_FLAGS, 73 new NdsInteger(SchemaClass.DS_CONTAINER_CLASS | SchemaClass.DS_EFFECTIVE_CLASS)); 74 75 // specify two values in the containment attribute 76 77 Attribute attr = new BasicAttribute( SchemaClass.ATTR_OBJECT_CONTAINMENT_CLASSES, 78 new NdsDistinguishedName("Organization")); 79 80 attr.add(new NdsDistinguishedName("Organizational Unit")); 81 attrs.put(attr); 82 83 // specify the Naming Attributes 84 85 attrs.put(SchemaClass.ATTR_OBJECT_NAMING_ATTRIBUTES, 86 new NdsDistinguishedName("CN")); 87 88 // specify Mandatory Attributes, naming attributes must be a mandatory attribute 89 90 attrs.put(SchemaClass.ATTR_OBJECT_MANDATORY_ATTRIBUTES, 91 new NdsDistinguishedName("CN")); 92 93 // specify the newly created custom attribute as an optional Attribute 94 95 attrs.put(SchemaClass.ATTR_OBJECT_OPTIONAL_ATTRIBUTES, 96 new NdsDistinguishedName(newAttr)); 97 98 // Create the custom class 99 100 schemaRoot.createSubcontext(newClass + ".classes", attrs); 101 System.out.println(newClass + " custom class created\n"); 102 103 // now create an actual instance of the new class 104 105 attrs = new BasicAttributes(); 106 attrs.put("Object Class", new NdsDistinguishedName(newClass)); 107 ctx.createSubcontext(newObject, attrs); 108 System.out.println(newObject + " object created\n"); 109 110 // now add the custom attribute to the new object 111 112 attrs = new BasicAttributes(); 113 attrs.put(newAttr, new NdsCaseExactString("value of the custom attribute")); 114 ctx.modifyAttributes(newObject, DirContext.ADD_ATTRIBUTE, attrs); 115 System.out.println(newAttr + " add to the object " + newObject); 116 117 // then get the attribute back, just to see that it worked 118 attrs = ctx.getAttributes(newObject, new String[] { newAttr }); 119 120 System.out.println("Object Name = " + newObject + 121 "\nAttribute Name = " + newAttr + 122 "\nValue = " + attrs.get(newAttr).get()); 123 } 124 catch (Exception e) 125 { 126 e.printStackTrace(); 127 } 128 } 129 }
Lines 11- 16. Initialize default values for the names of the custom class, custom attributes, object name, value of the custom attribute for the object, context and tree name.
Lines 19 - 27. Print a warning message to warn user not to run this sample in a production tree.
Lines 29 - 32. Process a user's response to decide if we can proceed to extend the schema.
Lines 34 - 44. Assign user-entered command line arguments to class name, attribute name, context and NDS treename.
Lines 46 - 49. Set the starting point for name resolution operation to the InitialContextFactoryclass of the NDS service provider and initialize the host to a specific NDS tree.
51 DirContext ctx = (DirContext) new InitialContext(env).lookup(context);
This line gets a DirContextby constructing an instance of the InitialContextand also invoking lookup()to retrieve the object by its name. The program sets the default value to "novell" which is the organization of this NDS tree. Your NDS tree will most probably have a different name for the context. You can either modify the hard-coded value in line 12 or override it by passing the context in as a command line argument. The same change will be needed for the "treeName" variable.
52 DirContext schemaRoot = ctx.getSchema("");
Get a DirContextreferring to the NDS schema by calling getSchema(String name)which returns the root of the schema information tree that is associated with the named object. NDS schema is global to the whole tree, so the NDS service provider decides that the entire directory shares the same schema.
Line 54 - 61. Create a custom attribute. JNDI schema is also implemented as a DirContext. The JNDI specification suggests that the root of a JNDI schema DirContext should contain three children: Attributes, Classes, and Syntaxes. The package com.novell.service.nds contains the following interfaces: SchemaAttribute, SchemaClass and SchemaSyntax. These interfaces show what attributes can be contained in an NDS class, an NDS attribute or an NDS syntax. The SchemaAttribute interface contains the following attributes: Syntax ID, Lower Limit, Upper Limit, ASN1Name, and Flags (flag is used to indicate single value, size, read only, non-removable, hidden, string,...). For a detailed explanation of the characteristics of an NDS attribute, please refer to the NDS and Bindery section at http://developer.novell.com/sdk/htmldoc/nwhtm/index.htm.
58 attrs = new BasicAttributes();
Create an attribute set to contain some basic attributes or characteristics that the SchemaAttribute DirContextwill contain. Every attribute must have a syntax, so a syntax ID is a required attribute.
59 attrs.put("Syntax ID", new NdsInteger(NdsSyntaxId.CASE_EXACT_STRING_ID));
We are going to create an attribute with the syntax of a "case exact string". NdsSyntaxId defines constants for all possible NDS syntaxes.
60 schemaRoot.createSubcontext(newAttr + ".attributes", attrs);
We got a DirContextreferring to the global schema by calling getSchema()in line 52. The schema returned by getSchema()is a SchemaDirContext object. So we can use its methodcreateSubcontext()to create a new DirContex t and bind it in the target context which is the "Attributes" DirContextunder the schema root. The first parameter to createSubcontext()is the name of the new context which is the concatenation of the string stored in the newAttr variable and the literal string ".attributes". This indicates that the new attribute will be added to the " Attributes" DirContext under the schema root.
Line 63 - 74. Create a custom class. The interface SchemaClassrepresents a single NDS class definition. A SchemaClassDirContextcontains the following attributes: Object Super Classes, Object Containment Classes, Object Naming Attributes, Object Mandatory Attributes, Object Optional Attributes, ASN1Name, and Flags. The NDS schema documentation has a detail explanation of what these attributes of an NDS class mean.
65 attrs = new BasicAttributes(); 66 67 attrs.put(SchemaClass.ATTR_OBJECT_SUPER_CLASSES, 68 new NdsDistinguishedName("Top"));
Make this class inherit from the "Top" class.
72 attrs.put(SchemaClass.ATTR_FLAGS, 73 new NdsInteger(SchemaClass.DS_CONTAINER_CLASS | SchemaClass.DS_EFFECTIVE_CLASS));
This sets the bits in the schema Flags attribute to indicate that this custom class is a container class which can contain subordinates and it is an effective class which can instantiate objects.
77 Attribute attr = new BasicAttribute( SchemaClass.ATTR_OBJECT_CONTAINMENT_CLASSES, 78 new NdsDistinguishedName("Organization")); 79 80 attr.add(new NdsDistinguishedName("Organizational Unit"));
Specify which classes can contain this custom class. Since this custom class can be contained by two classes: "Organization" and "Organizational Unit", the attribute for the containment class attribute is a multiple values attribute.
85 attrs.put(SchemaClass.ATTR_OBJECT_NAMING_ATTRIBUTES, 86 new NdsDistinguishedName("CN"));
The naming attribute is used in naming objects of that class. The naming or name-by attributes are the only attribute that can serve as part of the partial name for the objects of that class. This custom class is named by its CN (Common Name) attribute.
90 attrs.put(SchemaClass.ATTR_OBJECT_MANDATORY_ATTRIBUTES, 91 new NdsDistinguishedName("CN"));
A naming/name-by attribute must also be a mandatory attribute. That is why the mandatory attribute of this schema class is also set to the "CN" attribute.
95 attrs.put(SchemaClass.ATTR_OBJECT_OPTIONAL_ATTRIBUTES, 96 new NdsDistinguishedName(newAttr));
Add the newly created custom attribute as an optional attribute to the custom class.
100 schemaRoot.createSubcontext(newClass + ".classes", attrs); 101 System.out.println(newClass + " custom class created\n");
Calls createSubcontext()to create a new SchemaClass DirContextand binds it in the target context which is the "Classes" DirContext under the global schema root. The first parameter to createSubcontext() is the name of the new context which is the concatenation of the string stored in the newClassvariable and the literal string ". classes", this indicate that the new custom class will be add to the "Classes" DirContextunder the schema root.
Lines 105 to 108. Instantiate an object of the newly created custom class.
112 attrs = new BasicAttributes(); 113 attrs.put(newAttr, new NdsCaseExactString("value of the custom attribute"));
Instantiate an attribute of the new custom attribute type and supply a case exact string as a value of the attribute.
114 ctx.modifyAttributes(newObject, DirContext.ADD_ATTRIBUTE, attrs);
CallsmodifyAttributes() to add the attribute to the newly created object.
118 attrs = ctx.getAttributes(newObject, new String[] { newAttr });
Read the attribute back to verify the newly created object contains the custom attribute.
Conclusion
This article is intended to make it easier for you to jumpstart NDS development using Novell's Java Class Libraries (NJCL). This article does not cover Novell Java Beans which will make it even more simple for NDS development. Detailed documentation on JNDI and NJCL can be found on http://www.javasoft.com/products/jndiand http://developer.novell.com/ndk. Novell Developer Support also provides free support on the news forum at http://developer.novell.com/support. The sample code (versions with in-code comments) listed in this article can be downloaded at http://developer.novell.com/support/sample/areas/javas.htm.
* Originally published in Novell AppNotes
Disclaimer
The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.