Example 2: SimpleSite, a Web Site with User Profile Storage

   

Example 2: SimpleSite, a Web Site with User Profile Storage

Many Web sites allow people to register with the site to enter profile information and to support authenticated services. This section describes an application that implements a simple Web site where people can create personal profiles (including passwords for authentication), update their profiles, and search for other registered users. Although this sample application has a limited feature set, it shows one way to create an authenticated Web site with profile storage.

Directory Use

Our application is named SimpleSite. It uses a directory service for the following tasks :

  • User profile creation . When a new Web site visitor becomes a member of the SimpleSite Web site by registering his personal profile, a new entry is created in the directory.

  • User profile updates . When an authenticated member updates his personal profile, a directory entry is modified.

  • Password-based authentication . Members must log in to the Web application to access its features. The LDAP bind operation is used to verify each user's password.

  • Searching for other registered users . The SimpleSite application provides a Find page that has an LDAP directory search back end.

The SimpleSite application uses the inetOrgPerson object class from RFC 2798 as the structural class for user profile entries. Listing 21.15 shows the additional schema used. Although the listing shows placeholders for the OIDs (object identifiers), real OIDs should be used.

Listing 21.15 The SimpleSite Custom Schema
 # Schema for SimpleSite LDAP Application Example # # attribute types: # dn: cn=schema attributeTypes: (  simpleEmailFormat-oid  NAME 'simpleEmailFormat'     DESC 'preferred format for received messages, text or HTML'     EQUALITY caseIgnoreMatch     SYNTAX 1.3.6.1.4.1.1466.115.121.1.15  ) attributeTypes: (  simpleExpertise-oid  NAME 'simpleExpertise'     DESC 'areas of expertise'     EQUALITY caseIgnoreMatch     SYNTAX 1.3.6.1.4.1.1466.115.121.1.15  ) # # object classes: # objectClasses: (  simpleSiteObject-oid  NAME 'simpleSiteObject'     DESC 'simple site information'     SUP top     AUXILIARY     MAY ( co $ simpleEmailFormat $ simpleExpertise )  ) 

One auxiliary object class called simpleSiteObject is defined that includes two optional attribute types. This auxiliary class is added to all user entries. The co attribute holds the user's country. The simpleEmailFormat attribute holds a MIME type that specifies the user's preferred format for e-mail messages he receives, either text/plain (text format) or text/html (HTML format). This information could be used by the SimpleSite administrators when sending monthly newsletters or personalized e-mail messages.

The simpleExpertise attribute holds text that indicates the areas in which a registered user claims to have some expertise. For example, it could hold values such as Java programming or sewing . Listing 21.16 shows a sample user profile entry.

Listing 21.16 A SimpleSite User Profile Entry
 dn: mail=mcs@netscape.com,ou=Members,o=SimpleSite objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson objectClass: simpleSiteObject mail: mcs@netscape.com userPassword: secret cn: Mark Smith sn: Smith street: 100 S. Main Street L: Ann Arbor st: MI postalCode: 48104 co: US simpleEmailFormat: text/html simpleExpertise: LDAP,cooking 

The SimpleSite application uses a very simple DIT structure in which all registered users are placed under a configurable branch within a configurable top-level subtree . Entries are named by the mail attribute.

Tip

Although the SimpleSite application is a self-contained application and could use all custom schemas, it uses standard schema elements and one custom auxiliary class. This is a good example of how to extend LDAP schemas to support a specific application while maintaining compatibility with as many LDAP clients and servers as possible.


The User Experience

Users who interact with the SimpleSite application must first register a personal profile. Figure 21.13 shows the initial page that visitors see.

Figure 21.13. The SimpleSite Login Page

Clicking on the Register button brings up a Create New Profile form. Most of the fields are optional, but an e-mail address and password are required. Figure 21.14 shows a completed profile form that is about to be submitted.

Figure 21.14. The SimpleSite Create New Profile Page

Once a new visitor has registered or a returning member has logged in, he is given three options:

  1. To edit his profile

  2. To find other people

  3. To log out

The logout function simply discards all identity information and returns the user to the Login page. Figure 21.15 shows an example of the Edit Profile page. This page is straightforward in function.

Figure 21.15. The SimpleSite Edit Profile Page

The Find function is more interesting. A page containing a search form is presented, and the user is asked to fill in as many fields as he can. Figure 21.16 shows a SimpleSite Find page in which the user has filled in a city ("Redmond") and a state code ("WA").

Figure 21.16. The SimpleSite Find Page

When the user clicks on the Find button, a directory search is performed and all matching entries are returned. An exact match is used for all fields except Areas of Expertise , where a substring match is used. Figure 21.17 shows a sample Search Results page.

Figure 21.17. A SimpleSite Search Results Page

Below the information for the person or persons found, Send Email and Display Map links are provided. The Send Email link is a "mailto:" URL that typically causes a new e-mail window to be opened with the To: field already filled in. The Display Map link is an external "http:" link that points to the MapQuest map generation service. Figure 21.18 shows the result of clicking on Mr. Gates's Display Map link.

Figure 21.18. The Result of Clicking on a SimpleSite Display Map Link

The location information stored in the user's profile is used to create the link to MapQuest.

The Source Code

The SimpleSite application is written as a Java servlet, and it uses JNDI to access an LDAP directory service. The code was compiled and tested within the Web application container on Netscape Enterprise Server 6 running on the Sun Solaris 8 Unix operating system. The code should run on any Web server that provides a standard servlet container. The source code for the SimpleSite application consists of two HTML files and one Java file. No JSP tags are used; all HTML is generated directly by the Java code or through static HTML files. The code is presented here in pieces to aid understanding.

Listing 21.17 shows the login.htm file, which is the source for the page that is presented when someone first connects to the SimpleSite application (as shown in Figure 21.13). There are two HTML forms on this page: a "Login" form and a "Register" form. Both have an action that targets SimpleSiteServlet with extra path information used to specify a subcommand ( login or newprofile ). SimpleSiteServlet is the name of the servlet implemented by the Java code that will be described later.

Listing 21.17 The SimpleSite login.htm File
 <html> <head><title>SimpleSite - Login</title></head> <body> <div align="Center"><h2>SimpleSite - Login</h2></div> <h3>Members, please log in.</h3> <form action="SimpleSiteServlet/login" method="POST">   <table cellpadding="2" cellspacing="2" border="0"><tbody>        <tr>          <td>EMail Address:</td>          <td><input type="text" name="email" size="30"></td>        </tr>        <tr>          <td>Password:</td>          <td><input type="password" name="pwd" size="30"></td>        </tr>   </tbody></table>   <p>   <input type="submit" value="Login"> </form> <hr> <h3>New visitors, please click the Register button.</h3> <form action="SimpleSiteServlet/newprofile" method="GET">   <input type="submit" value="Register"> </form> </body> </html> 

Listing 21.18 shows the find.htm file, which provides the initial user interface for the SimpleSite Find page. Several input fields are provided (for example, City ). The input field names are LDAP attribute names . The form action again points to SimpleSiteServlet , this time with a subcommand of find .

Listing 21.18 The SimpleSite find.htm File
 <html> <head><title>SimpleSite - Find</title></head> <body> <div align="Center"><h2>SimpleSite - Find</h2></div> <h3>Fill in as much information as you know about the other person.</h3> <form action="SimpleSiteServlet/find" method="POST">   <table cellpadding="2" cellspacing="2" border="0"><tbody>        <tr>          <td>EMail Address:</td>          <td><input type="text" name="mail" size="40"></td>        </tr>        <tr>          <td>Name:</td>          <td><input type="text" name="cn" size="40"></td>        </tr>        <tr>          <td>City:</td>          <td><input type="text" name="L" size="40"></td>        </tr>        <tr>          <td>State:</td>          <td><input type="text" name="st" size="4"></td>        </tr>        <tr>          <td>Areas of Expertise:</td>          <td><input type="text" name="simpleExpertise" size="50"></td>        </tr>   </tbody></table>   <p>   <input type="submit" value="Find">   <input type="button" value="Return to Previous Page" onClick="window.history.back()"> </form> </body> </html> 

The servlet source code is in a file named SimpleSiteServlet.java . Listing 21.19 shows the beginning of this file.

Listing 21.19 The First Part of SimpleSiteServlet.java
 1. /*  2.  * SimpleSite Servlet LDAP Application Example.  3.  *  4.  * From the 2nd Edition of the book:  5.  *   "Understanding and Deploying LDAP Directory Services"  6.  *   by Timothy A. Howes, Mark C. Smith, and Gordon S. Good.  7.  */  8. import java.io.*;  9. import java.util.*; 10. import javax.servlet.*; 11. import javax.servlet.http.*; 12. import javax.naming.*; 13. import javax.naming.directory.*; 14. 15. public class SimpleSiteServlet extends HttpServlet 16. { 17. /* 18.  * Data structures. 19.  */ 20. private class fieldmap { 21.     boolean     mNewProfileOnly; 22.     String      mHtmlFieldName; 23.     int         mHtmlFieldSize; 24.     String      mLdapAttrName; 25.     String      mMapQuestFieldName; 26. 27.     // fieldmap constructor 28.     private fieldmap( boolean newProfileOnly, String htmlFieldName, 29.                 int htmlFieldSize, String ldapAttrName, 30.                 String mapQuestFieldName ) { 31.         mNewProfileOnly = newProfileOnly; 32.         mHtmlFieldName = htmlFieldName; 33.         mHtmlFieldSize = htmlFieldSize; 34.         mLdapAttrName = ldapAttrName; 35.         mMapQuestFieldName = mapQuestFieldName; 36.     } 37. }; 38. 

The Java packages used are imported by the code on lines 8 to 13. The packages that begin with "javax.naming" are part of JNDI. The SimpleSiteServlet class is declared on line 15; it is derived from the standard Java HttpServlet class. The remainder of the code (lines 20 “37) defines a private class named fieldmap that is able to store information for one user-visible data field. The fieldmap class is a simple data structure (it has only one method, a constructor). The fieldmap member variables are

  • mNewProfileOnly . A Boolean that indicates whether this data field should be displayed only on the New Profile page.

  • mHtmlFieldName . A human-readable name for the field that is used to label HTML form elements.

  • mHtmlFieldSize . The HTML field size that is used to construct the size= attribute of an HTML input form element.

  • mLdapAttrName . The LDAP attribute type for the data field.

  • mMapQuestFieldName . The label used for this field within a MapQuest Show Map URL. If null , the field is not used when constructing a Show Map URL.

Listing 21.20 shows the second portion of the Java source file. The code on lines 42 to 48 defines some SimpleSiteServlet member variables, most of which are set from context initialization properties that appear in an external Web application file (the code that does that is shown later, in Listing 21.21).

Listing 21.20 The Second Part of SimpleSiteServlet.java
 39. /* 40.  * Private data. 41.  */ 42. private String  mApplName    = "SimpleSite"; 43. private String  mMQURLPrefix = "http://www.mapquest.com/maps/map.adp?"; 44. private String  mLdapURL;       // URL for the LDAP server 45. private String  mBaseDN;        // from "ldapBase" property 46. private String  mAddLocation;   // from "ldapAddLocation" property 47. private String  mAdder;         // from "ldapAdder" property 48. private String  mAdderPwd;      // from "ldapAdderPwd" property 49. 50. private fieldmap[] mProfileFields = { 51.   new fieldmap( true,  "Email Address",  50, "mail",           null ), 52.   new fieldmap( true,  "Password",       30, "userPassword",   null ), 53.   new fieldmap( false, "Name",           30, "cn",             null ), 54.   new fieldmap( false, "Street Address", 40, "street",    "address" ), 55.   new fieldmap( false, "City",           40, "L",            "city" ), 56.   new fieldmap( false, "State",           4, "st",          "state" ), 57.   new fieldmap( false, "Zip Code",       12, "postalCode", "zipcode" ), 58.   new fieldmap( false, "Country",         4, "co",        "country" ), 59.   new fieldmap( false, "Areas of Expertise", 60.                                             50, "simpleExpertise",  null ), 61.   new fieldmap( false, "Preferred Email Format", 62.                                              0, "simpleEmailFormat",null ) 63. }; 64. 65. private String[] mLdapAttrsToRetrieve; 66. 67. private String[] mObjectClassValues = { 68.     "inetOrgPerson", 69.     "simpleSiteObject" 70. }; 71. 

The code on lines 50 to 63 creates a member variable named mProfileFields and initializes it with an array of fieldmap objects. For example, on line 55 a fieldmap object is created that has an HTML input field name of City , an HTML field size of 40 characters , an LDAP attribute name of L (locality), and a MapQuest URL field name of city .

The code on lines 65 to 70 defines two additional member variables. The mLdapAttrs ToRetrieve variable is an array of LDAP attributes to retrieve when searching for entries (the array is created by code we will look at later). The mObjectClassValues variable is a statically initialized array that includes the two LDAP object class values that must be included in all new entries ( inetOrgPerson and simpleSiteObject ).

The SimpleSiteServlet class has only three public methods :

  1. init() . Called once when the servlet is loaded.

  2. doGet() . Called when an HTTP client (typically a Web browser) submits an HTTP GET request that targets the servlet.

  3. doPost() . Called when an HTTP client submits an HTTP POST request that targets the servlet.

The Java servlet container (typically part of a Web server or an application server) ensures that these public methods are called. Listing 21.21 shows the init() method.

Listing 21.21 The SimpleSiteServlet init() Method
 72.  73. /*  74.  * Public servlet methods.  75.  */  76.     /*  77.      * init(): Perform essential initialization  78.      */  79.     public void init(  80.             ServletConfig config )  81.             throws ServletException {  82.         super.init( config );  83.  84.         // initialize our base DN, member RDN, and LDAP URL  85.         mBaseDN = getServletContext().getInitParameter( "ldapBase" );  86.         mAddLocation = getServletContext().getInitParameter(  87.                 "ldapAddLocation" );  88.         mLdapURL = "ldap://"  89.                 + getServletContext().getInitParameter( "ldapServer")  90.                 + "/" + mBaseDN;  91.  92.         // initialize the DN and password we use to create new entries  93.         mAdder = getServletContext().getInitParameter( "ldapAdder" );  94.         mAdderPwd = getServletContext().getInitParameter( "ldapAdderPwd" );  95.  96.         // create a list of LDAP attributes we will retrieve  97.         mLdapAttrsToRetrieve = new String[ mProfileFields.length ];  98.         for ( int i = 0; i < mProfileFields.length; ++i ) {  99.             mLdapAttrsToRetrieve[i] = mProfileFields[i].mLdapAttrName; 100.         } 101.     } 

The init() code calls its parent's init() method on line 82, as required by the servlet specification. The code on lines 84 to 94 retrieves a set of context initialization properties from the servlet container and saves them in member variables. For example, line 85 retrieves the ldapBase property and stores it in a member variable named mBaseDN . Later this value is used as the base for LDAP searches, and the ldapAddLocation property is prepended to the mBaseDN value to determine the parent DN for new entries. The code on lines 96 to 100 initializes the mLdapAttrsToRetrieve array by copying the LDAP attribute names from the mProfileFields fieldmap array.

Tip

The SimpleSite application retrieves a series of LDAP configuration parameters from the servlet container. The LDAP server, port, base DN for searches, DN and password used in the addition of entries, and the location where new entries are added can all be changed to match a specific directory deployment or test bed. Typically, servlet parameters like these are stored in an XML configuration file, which means that the parameters can be modified without the SimpleSite code having to be recompiled. You should use similar techniques in your own LDAP applications to avoid hard-coding knowledge of a particular directory deployment (or server location within a deployment) in your code.


Listing 21.22 shows the doGet() method, which is called by the servlet container in response to an HTTP GET request. The doGet() method examines the extra path information from the request URL and calls one of these three methods to handle the request:

  1. doNewProfile() . Displays a Create New Profile form, as shown in Figure 21.14.

  2. doEditProfile() . Displays the Edit Profile form with the current user's profile data filled in. Figure 21.15 shows an example of such a form.

  3. doLogout() . Clears all authentication and identity information and redirects the browser to the Login page.

The source code for each of these methods is shown later, in Listings 21.28 and 21.32.

Listing 21.22 The SimpleSiteServlet doGet() Method
 102. 103.     /* 104.      * doGet(): handle an HTTP GET request 105.      */ 106.     public void doGet( 107.             HttpServletRequest request, 108.             HttpServletResponse response ) 109.             throws IOException, ServletException { 110. 111.         // prepare for response generation and dispatch to handler method 112.         response.setContentType( "text/html" ); 113.         PrintWriter writer = response.getWriter(); 114. 115.         String operation = request.getPathInfo(); 116.         if ( operation == null ) { 117.             operation = ""; 118.         } else { 119.             operation = operation.substring( 1 ); 120.         } 121. 122.         if ( operation.equals( "editprofile" )) { 123.             doEditProfile( request, response, writer ); 124.         } else if ( operation.equals( "newprofile" )) { 125.             doNewProfile( request, response, writer ); 126.         } else if ( operation.equals( "logout" )) { 127.             doLogout( request, response, writer ); 128.         } else { 129.             unknownRequest( operation, "HTTP GET", writer ); 130.         } 131.     } 

Listing 21.23 shows the doPost() method, which is called by the servlet container in response to an HTTP POST request.

Listing 21.23 The SimpleSiteServlet doPost() Method
 132. 133.     /* 134.      * doPost(): handle an HTTP POST request 135.      */ 136.     public void doPost( 137.             HttpServletRequest request, 138.             HttpServletResponse response ) 139.             throws IOException, ServletException { 140. 141.         // prepare for response generation and dispatch to handler method 142.         response.setContentType( "text/html" ); 143.         PrintWriter writer = response.getWriter(); 144. 145.         String operation = request.getPathInfo(); 146.         if ( operation == null ) { 147.             operation = ""; 148.         } else { 149.             operation = operation.substring( 1 ); 150.         } 151. 152.         if ( operation.equals( "login" )) { 153.             doLogin( request, response, writer ); 154.         } else if ( operation.equals( "find" )) { 155.             doFind( request, response, writer ); 156.         } else if ( operation.equals( "saveprofile" )) { 157.             doSaveProfile( request, response, writer ); 158.         } else { 159.             unknownRequest( operation, "HTTP POST", writer ); 160.         } 161.     } 

Similar to how doGet() works, the doPost() method examines the extra path information from the request URL and calls one of these three methods to handle the request:

  1. doLogin() . Processes the Login form and authenticates the user against the directory service.

  2. doFind() . Processes the Find form, searches the directory, and displays results like those shown in Figure 21.17.

  3. doSaveProfile() . Processes input from the Create New Profile and Edit Profile forms, performing an LDAP add or modify operation as appropriate.

The source code for the doFind() and doSaveProfile() methods is shown later, in Listings 21.30, 21.34, and 21.35. Listing 21.24 shows the doLogin() method.

Listing 21.24 The SimpleSiteServlet doLogin() Method
 162. 163. /* 164.  * Operation handlers. 165.  */ 166.     /* 167.      * doLogin(): handle a "login" HTTP POST sub-request 168.      */ 169.     private void doLogin( 170.             HttpServletRequest request, 171.             HttpServletResponse response, 172.             PrintWriter writer ) { 173. 174.         // output the page header 175.         writePageHeader( mApplName + " - Login", writer ); 176. 177.         // retrieve the HTTP session 178.         HttpSession httpsession = request.getSession(); 179. 180.         // retrieve the login form variables 181.         String email = request.getParameter( "email" ); 182.         if ( email == null  email.length() == 0 ) { 183.             reportError( "Login: please enter your email address", 184.                     null, writer ); 185.             return; 186.         } 187.         String pwd = request.getParameter( "pwd" ); 188.         if ( pwd == null  pwd.length() == 0 ) { 189.             reportError( "Login: please enter your password", null, writer ); 190.             return; 191.         } 192. 193.         clearSessionValues( httpsession ); 194. 195.         try { 196.             DirContext ctx = createLDAPContext( null ); 197. 198.             // map the e-mail address to a DN 199.             String partialDN = email2LDAPDN( ctx, email, writer ); 200.             ctx.close(); 201. 202.             if ( partialDN != null ) { 203.                 // try to authenticate 204.                 ctx = createLDAPContext( partialDN, pwd ); 205. 206.                 // store user information in the HTTP session 207.                 httpsession.setAttribute( "id", email ); 208.                 httpsession.setAttribute( "partialDN", partialDN ); 209.                 httpsession.setAttribute( "pwd", pwd ); 210. 211.                 // finish the page and close the LDAP connection 212.                 writePageFooter( "<h3>Authentication succeeded.</h3>", 213.                         true, writer  ); 214.                 ctx.close(); 215.             } 216.         } catch ( Exception e ) { 217.             reportError( "Login:", e, writer ); 218.         } 219.     } 

Line 175 calls the writePageHeader() utility method to generate the beginning of an HTML page, including a title (the code for writePageHeader() is shown later, in Listing 21.36). The code on line 178 retrieves the current HTTP session, creating one if necessary. The standard servlet infrastructure provides a shared HTTP session that can be used to store arbitrary session attributes. The HTTP session is typically implemented through HTTP cookies or through URL rewriting, although servlet writers do not need to worry about the details.

The SimpleSiteServlet() method uses three session attributes:

  1. id . The e-mail address of the logged-in user ”for example, mcs@netscape.com .

  2. partialDN . The partial LDAP DN (relative to the mBaseDN variable) of the logged-in user ”for example, mail=mcs@netscape.com,ou=Members .

  3. pwd . The LDAP password of the logged-in user ”for example, secret .

The doLogin() method sets the session attributes (lines 206 “209). Other methods, such as doSaveProfile() , retrieve the session attributes and use them to identify the user and to authenticate to the directory service.

The code on lines 195 to 218 handles the LDAP-based authentication. Line 196 includes a call to a createLDAPContext() utility method, whose job is to create a JNDI LDAP directory context (the code for createLDAPContext() is shown next , in Listing 21.25). Line 199 calls a utility method named email2LDAPDN() to find the partial LDAP DN (again, relative to the mBaseDN variable) that matches the e-mail address entered on the Login form. The code for email2LDAPDN() is shown later. Line 204 tries to create an authenticated JNDI directory context, and if it is successful, the session attributes are set and a success message is sent to the user. Errors are displayed via the reportError() utility method (shown later, in Listing 21.37).

Listing 21.25 shows two createLDAPContext() utility methods (this is an example of Java method overloading, in which two methods have the same name but different parameter lists). These methods demonstrate how to create a JNDI LDAP directory context, optionally with simple (password-based) authentication. Listing 21.25 shows methods that are located near the end of the SimpleSiteServlet.java source file; that's why the line numbers do not begin where Listing 21.24 left off (the SimpleSiteServlet methods are presented out of order to make it easier to follow the flow between methods).

Listing 21.25 The SimpleSiteServlet createLDAPContext() Methods
 585. 586. /* 587.  * LDAP utility methods. 588.  */ 589.     /* 590.      * createLDAPContext(): create an initial directory 591.      *    context (open the LDAP connection). If partialDN is 592.      *    not null, simple authentication is done. 593.      */ 594.     private DirContext createLDAPContext( 595.             String      partialDN,  // can be null 596.             String      pwd ) 597.             throws NamingException { 598. 599.         // initialize basic environment for JNDI's LDAP provider 600.         Hashtable env = new Hashtable(); 601.         env.put( Context.INITIAL_CONTEXT_FACTORY, 602.                 "com.sun.jndi.ldap.LdapCtxFactory" ); 603.         env.put( Context.PROVIDER_URL, mLdapURL ); 604.         env.put( Context.REFERRAL, "follow" ); 605. 606.         // handle optional simple bind 607.         if ( partialDN != null && partialDN.length() > 0 ) { 608.             String dn = partialDN + "," + mBaseDN; 609.             env.put( Context.SECURITY_AUTHENTICATION, "simple" ); 610.             env.put( Context.SECURITY_PRINCIPAL, dn ); 611.             env.put( Context.SECURITY_CREDENTIALS, pwd ); 612.         } 613.         return new InitialDirContext( env ); 614.     } 615. 616.     /* 617.      * createLDAPContext(): create an initial directory context based 618.      *    on information found in an HTTP session. 619.      */ 620.     private DirContext createLDAPContext( 621.             HttpSession httpsession )   // can be null 622.             throws NamingException { 623. 624.         String dn = null, pwd = null; 625.         if ( httpsession != null ) { 626.             dn = (String)httpsession.getAttribute( "partialDN" ); 627.             pwd = (String)httpsession.getAttribute( "pwd" ); 628.         } 629.         return createLDAPContext( dn, pwd ); 630.     } 

The first method takes two String parameters: partialDN and pwd . The second method takes one parameter: httpsession . Both methods create a JNDI LDAP directory context, which is essentially a connection to an LDAP server, along with some associated state information that is used by JNDI. The second method extracts the partial DN and password from the HTTP session and simply calls the first method.

JNDI uses a hash table to pass directory-specific parameters into the InitialDirContext constructor. The code on line 600 creates a hash table named env (short for environment). Sun's LDAP provider is selected by lines 601 and 602 (JNDI allows other LDAP and non-LDAP providers to be used; for example, Netscape includes an LDAP provider in its LDAP Java SDK). The code on line 603 adds to the hash table an LDAP URL containing the server, port, and base DN, and line 604 enables automatic following of LDAPv3 referrals.

If the partialDN and password ( pwd ) are not null , an authenticated LDAP connection is requested . The code on lines 606 to 612 adds the necessary information to the env hash table. Finally, the code on line 613 calls the InitialDirContext constructor to establish the LDAP connection and perform authentication (if requested).

Listing 21.26 shows the email2LDAPDN() utility method that is called from doLogin() to return a partial LDAP DN, given an e-mail address. The email2LDAPDN() code demonstrates how to perform an LDAP search using JNDI. An LDAP directory context ( DirContext ) must be passed to email2LDAPDN() , along with an e-mail address String and a PrintWriter that is associated with the HTML response (used for reporting errors).

Listing 21.26 The SimpleSiteServlet email2LDAPDN() Method
 631. 632.     /* 633.      * email2LDAPDN(): Find a person entry based on an e-mail address. 634.      *    Return a partial DN that is context-relative. 635.      */ 636.     private String email2LDAPDN( 637.             DirContext ctx, 638.             String email, 639.             PrintWriter writer ) { 640. 641.         String partialDN = null; 642. 643.         // construct the search filter and search controls 644.         String filter = "(&(objectClass=person)(mail=" + 645.                 escapedValue( email ) + "))"; 646.         String[] attrlist = { "1.1" };  // all we need is the DN 647.         SearchControls ctrls = new SearchControls( 648.                 SearchControls.SUBTREE_SCOPE, 649.                 100,                    // size limit 650.                 1000 * 15,              // 15s time limit 651.                 attrlist,               // attrs to retrieve 652.                 false,                  // return entire object 653.                 false                   // dereference aliases 654.                 ); 655. 656.         // do the search 657.         try { 658.             NamingEnumeration answer = ctx.search( "", filter, ctrls ); 659. 660.             int count = 0; 661.             while ( answer.hasMore()) { 662.                 SearchResult sr = (SearchResult)answer.next(); 663.                 if ( count == 0 ) { 664.                     partialDN = sr.getName(); 665.                 } 666.                 ++count; 667.             } 668. 669.             if ( count == 0 ) { 670.                 reportError( "No matches for " + email, null, writer ); 671.                 partialDN = null; 672.             } else if ( count > 1 ) { 673.                 reportError( count + " matches for " + email, null, writer ); 674.                 partialDN = null; 675.             } 676.         } catch ( Exception e ) { 677.             reportError( "Email lookup :", e, writer ); 678.         } 679. 680.         return partialDN; 681.     } 

An LDAP search filter of the form (&(objectClass=person)(mail=ESCAPED-VALUE)) is created by the code on lines 644 and 645. The escapedValue() utility method (shown next, in Listing 21.27) is used to escape all special LDAP filter characters within the search string. The code on lines 646 “654 creates a JNDI SearchControls object that includes information such as LDAP size and time limits and a list of attributes to retrieve. Because only the DN is needed by the email2LDAPDN() method, the special string "1.1" is the only element in the attrlist (indicating that no attributes should be returned).

Listing 21.27 shows the escapedValue() utility method. This method works by making a copy of the value that is passed in, escaping the appropriate characters by replacing each one with a backslash ( \ ) followed by the two-digit hexadecimal representation of the character. The specialChars local variable that is defined on line 689 contains the characters that are escaped ”namely, * , ( , ) , and \ . This set of characters comes from Section 4 of RFC 2254.

Listing 21.27 The SimpleSiteServlet escapedValue() Method
 682. 683.     /* 684.      * escapedValue(): produce a copy of a string value, but with 685.      *    special LDAP filter characters escaped as \HH (hex value). 686.      */ 687.     private String escapedValue( String value ) { 688.         StringBuffer escapedBuf = new StringBuffer(); 689.         String specialChars = "*()\"; 690.         char c; 691. 692.         for ( int i = 0; i < value.length(); ++i ) { 693.             c = value.charAt( i ); 694.             if ( specialChars.indexOf( c ) >= 0 ) { // escape it 695.                 escapedBuf.append( '\' ); 696.                 String hexString = Integer.toHexString( c ); 697.                 if ( hexString.length() < 2 ) { 698.                     escapedBuf.append( '0' ); 699.                 } 700.                 escapedBuf.append(hexString ); 701.             } else { 702.                 escapedBuf.append( c ); 703.             } 704.         } 705.         return escapedBuf.toString(); 706.     } 

Now that some of the LDAP- related utility methods have been shown, we return to our discussion of the methods that process HTTP GET and POST requests . Listing 21.28 shows the doLogout() method. This method is very simple. The HTTP session information is cleared by the call on line 231. Then a utility method named getIDWithRedirect() is called to send an HTTP redirect to the browser to return the user to the SimpleSite login page. The getIDWithRedirect() method attempts to retrieve the id session attribute, and sends a redirect if id can't be retrieved. Because the doLogout() method clears all of the session attributes before calling getIDWithRedirect() , a redirect is always issued in this case.

Listing 21.28 The SimpleSiteServlet doLogout() Method
 220. 221.     /* 222.      * doLogout(): handle a "logout" HTTP GET subrequest 223.      */ 224.     private void doLogout( 225.             HttpServletRequest request, 226.             HttpServletResponse response, 227.             PrintWriter writer ) { 228. 229.         // retrieve the HTTP session and clear all values 230.         HttpSession httpsession = request.getSession(); 231.         clearSessionValues( httpsession ); 232. 233.         // since the session info is gone, this will always redirect 234.         String id = getIDWithRedirect( httpsession, response, writer ); 235.     } 

Listing 21.29 shows the getIDWithRedirect() and clearSessionValues() methods. These two methods are straightforward, using the HttpSession getAttribute() method (line 765), the setAttribute() method (lines 784 “786), and the HttpServletResponse sendRedirect() method (line 769).

Listing 21.29 The SimpleSiteServlet getIDWithRedirect() and clearSessionValues() Methods
 752. 753. /* 754.  * General utility methods. 755.  */ 756.     /* 757.      * getIDWithRedirect(): Get the user's id from the HTTP session, 758.      *     redirecting to the login page if not present. 759.      */ 760.     private String getIDWithRedirect( 761.             HttpSession httpsession, 762.             HttpServletResponse response, 763.             PrintWriter writer ) { 764. 765.         String id = (String)httpsession.getAttribute( "id" ); 766. 767.         if ( id == null  id.length() == 0 ) { 768.             try { 769.                 response.sendRedirect( "login.htm" ); 770.             } catch ( Exception e ) { 771.                 reportError( "Redirect to login:", e, writer ); 772.             } 773.             id = null; 774.         } 775. 776.         return id; 777.     } 778. 779.     /* 780.      * clearSessionValues(): clear old information that may be 781.      *     in the HTTP session. 782.      */ 783.     private void clearSessionValues( HttpSession httpsession ) { 784.         httpsession.setAttribute( "id", "" ); 785.         httpsession.setAttribute( "partialDN", "" ); 786.         httpsession.setAttribute( "pwd", "" ); 787.     } 

Next we examine the doFind() method that is called when an HTTP POST request is received with extra path information of find . Listing 21.30 shows the doFind() implementation.

Listing 21.30 The SimpleSiteServlet doFind() Method
 236. 237.     /* 238.      * doFind(): handle a "find" HTTP POST subrequest 239.      */ 240.     private void doFind(    // HTTP POST method 241.             HttpServletRequest request, 242.             HttpServletResponse response, 243.             PrintWriter writer ) { 244. 245.         // retrieve the HTTP session 246.         HttpSession httpsession = request.getSession(); 247. 248.         // generate start of page 249.         String id = getIDWithRedirect( httpsession, response, writer ); 250.         if ( id == null ) return; 251.         writePageHeader( mApplName + " - Search Results", writer ); 252. 253.         // Retrieve form variables and create an ANDed search filter. 254.         // For each value, we create an equality filter component, 255.         // except for "Areas of Expertise" where a substring 256.         // component is used if 3 characters or more were entered. 257.         StringBuffer filter = new StringBuffer(); 258.         Enumeration params = request.getParameterNames(); 259.         while ( params.hasMoreElements()) { 260.             String paramName = (String)params.nextElement(); 261.             String paramVal = request.getParameter( paramName ); 262.             if ( paramVal != null && paramVal.length() > 0 ) { 263.                 if ( paramName.equalsIgnoreCase( "simpleExpertise" ) 264.                         && paramVal.length() >= 3 ) { 265.                     filter.append( '(' + paramName + "=*" + 266.                             escapedValue( paramVal ) + "*)" ); 267.                 } else { 268.                     filter.append( '(' + paramName + '=' + 269.                             escapedValue( paramVal ) + ')' ); 270.                 } 271.             } 272.         } 273.         if ( filter.length() == 0 ) { 274.             reportError( "Find: please provide some information", 275.                     null, writer ); 276.             return; 277.         } 278. 279.         // restrict our search to person entries only 280.         filter.insert( 0, "(&(objectClass=person)" ); 281.         filter.append( ')' ); 282. 283.         // search 284.         try { 285.             DirContext ctx = createLDAPContext( httpsession ); 286. 287.             SearchControls ctrls = new SearchControls( 288.                     SearchControls.SUBTREE_SCOPE, 289.                     10,                     // size limit 290.                     1000 * 15,              // 15s time limit 291.                     mLdapAttrsToRetrieve,   // attr list 292.                     false,                  // return entire object 293.                     false                   // dereference aliases 294.                     ); 295.             NamingEnumeration answer = ctx.search( "", 296.                     filter.toString(), ctrls ); 297. 298.             int count = 0; 299.             SearchResult sr = null; 300.             while ( answer.hasMore()) { 301.                 sr = (SearchResult)answer.next(); 302.                 displayOneEntry( sr.getAttributes(), writer ); 303.                 ++count; 304.             } 305. 306.             if ( count == 0 ) { 307.                 reportError( "No matches", null, writer ); 308.             } else if ( count > 1 ) { 309.                 writer.println( "<h3>" + count + " people found.</h3>" ); 310.             } 311. 312.             writePageFooter( null, true, writer ); 313. 314.         } catch ( Exception e ) { 315.             reportError( "Find:", e, writer ); 316.         } 317.     } 

On line 249, the getIDWithRedirect() method is called to ensure that the user is authenticated (if not, no id attribute is present in the HTTP session and the user is redirected to the login page). The code on lines 253 to 281 constructs an LDAP search filter based on the form variables that were part of the HTTP POST request. The search filter consists of a series of ANDed components , where each component is a simple equality filter. The one exception is that an inner substring filter component of the form (simpleExpertise= *ESCAPED-VALUE*) is used for the simpleExpertise LDAP attribute.

This approach for constructing the search filter relies on the fact that the HTML input field names are LDAP attribute names, as Listing 21.18 showed. The complete filter is of the following form: (&(objectClass=person)(ATTR1=ESCAPED-VALUE1)(ATTR2= ESCAPED-VALUE2)...) . For example, a search for people who live in the state of Washington and who claim to have expertise in software would look like this: (&(objectClass=person)(st=WA)(simpleExpertise=*software*)) .

Tip

The SimpleSiteServlet doFind() method avoids creating LDAP search filters that include short substring filter components, and it avoids approximate filter components entirely. Why? Because search filters that contain short substrings and approximate components tend to put a greater load on directory servers and cause slow searches. The best search filter to use depends on what kind of application you're creating and the preferences of the users of the application, but use simple, efficient filters whenever possible.


The remaining code in the doFind() method issues an LDAP search operation and processes the results. This code is very similar to that found in the email2LDAPDN() method examined earlier (see Listing 21.26), except that all of the attributes used by the SimpleSite application are requested (line 291), and each entry returned is displayed by a call to a utility method called displayOneEntry() . JNDI returns search results as a NamingEnumeration , and the code on lines 298 to 304 steps through the enumeration, using the getAttributes() method to retrieve the LDAP attributes, which are then passed to displayOneEntry() .

Listing 21.31 shows the displayOneEntry() method, which generates an HTML table with the human-readable field names in the first column and the user profile values in the second column. This method demonstrates how to retrieve a specific LDAP attribute from a JNDI Attributes object. The heart of the displayOneEntry() method is the for loop that starts on line 718. It loops over the elements of the mProfileFields fieldmap array, retrieving LDAP attribute values and emitting an HTML table row for each element. The code on lines 719 to 723 uses the Attributes get() method to retrieve an attribute by name, and the code on line 724 retrieves a single String value.

Listing 21.31 The SimpleSiteServlet displayOneEntry() Method
 707. 708.     /* 709.      * Display one person's profile information. 710.      */ 711.     private void 712.     displayOneEntry( Attributes attrs, PrintWriter writer ) 713.                 throws NamingException { 714.         String mqURL = mMQURLPrefix; 715.         String email = null; 716. 717.         writer.println( "<table border=\"0\"><tbody>" ); 718.         for ( int i = 0; i < mProfileFields.length; ++i ) { 719.             Attribute attr = attrs.get( 720.                     mProfileFields[i].mLdapAttrName ); 721.             if ( attr == null ) { 722.                 continue; 723.             } 724.             String val = (String)attr.get(); 725.             if ( val != null && val.length() > 0 ) { 726.                 writer.println( "<tr><td><b>" + 727.                         mProfileFields[i].mHtmlFieldName + 728.                         ":</b></td>\n<td>" + val + "</td></tr>" ); 729. 730.                 if ( mProfileFields[i].mMapQuestFieldName != null ) { 731.                     mqURL = mqURL + "&" + 732.                             mProfileFields[i].mMapQuestFieldName + 733.                             "=" + val; 734.                 } 735. 736.                 if ( mProfileFields[i].mLdapAttrName.equalsIgnoreCase( 737.                             "mail" )) { 738.                     email = val; 739.                 } 740.             } 741.         } 742.         writer.println( "</tbody></table>\n" ); 743. 744.         if ( email != null ) { 745.             writer.println( "<a href=\"mailto:" + email + 746.                     "\">Send Email</a>&nbsp;&nbsp;&nbsp" ); 747.         } 748.         writer.println( "<a href=\"" + mqURL + "\" target=_new>" 749.             + "Display Map</a><p>" ); 750.     } 751. 

After the for loop is complete, two HTML links are added to the page being generated: a Send Email "mailto:" link based on the mail attribute and a Display Map "http:" link to MapQuest's site. The MapQuest link is constructed from the attributes that have a non- null mMapQuestFieldName member variable in their fieldmap object.

Listing 21.32 shows the code for the two remaining HTTP GET request handler methods: doNewProfile() and doEditProfile() . Both of these methods call a utility method named emitProfileForm() (shown in Listing 21.33) that writes out an HTML form that has input fields for the user's profile information.

Listing 21.32 The SimpleSiteServlet doNewProfile() and doEditProfile() Methods
 318. 319.     /* 320.      * doNewProfile(): handle a "newprofile" HTTP GET subrequest 321.      */ 322.     private void doNewProfile( 323.             HttpServletRequest request, 324.             HttpServletResponse response, 325.             PrintWriter writer ) { 326. 327.         clearSessionValues( request.getSession()); 328.         writePageHeader( mApplName + " - Create New Profile", writer ); 329.         try { 330.             emitProfileForm( null, writer ); 331.         } catch ( Exception e ) { 332.             reportError( "New profile:", e, writer ); 333.         } 334.     } 335. 336.     /* 337.      * doEditProfile(): handle an "editprofile" HTTP GET subrequest 338.      */ 339.     private void doEditProfile( 340.             HttpServletRequest request, 341.             HttpServletResponse response, 342.             PrintWriter writer ) { 343. 344.         // retrieve the HTTP session 345.         HttpSession httpsession = request.getSession(); 346. 347.         // retrieve the ID plus DN and generate start of page 348.         String id = getIDWithRedirect( httpsession, response, writer ); 349.         if ( id == null ) return; 350.         String partialDN = (String)httpsession.getAttribute( "partialDN" ); 351.         writePageHeader( "Edit Profile for " + id, writer ); 352. 353.         try { 354.             // retrieve the user's profile information from the DS 355.             DirContext ctx = createLDAPContext( httpsession ); 356. 357.             Attributes attrs = ctx.getAttributes( partialDN, 358.                     mLdapAttrsToRetrieve ); 359.             emitProfileForm( attrs, writer ); 360.             ctx.close(); 361.         } catch ( Exception e ) { 362.             reportError( "Edit Profile:", e, writer ); 363.         } 364.     } 365. 

The doNewProfile() code is straightforward: It clears the HTTP session information and calls emitProfileForm() with a null first parameter (to indicate that no existing data is to be used on the HTML form).

The doEditProfile() code is only a little more complex. The code on lines 348 to 350 retrieves the session information, including the e-mail address ( id ) and the partial DN that identifies the authenticated user. That partial DN is passed to the JNDI getAttributes() method of the DirContext object. The getAttributes() method reads one entry by issuing an LDAP search with a base scope. Most JNDI methods expect names to be relative to the directory context, which is why a partial DN is typically used rather than a full DN.

Listing 21.33 shows the emitProfileForm() utility method that is called by doNewProfile() and doEditProfile() . Like the displayOneEntry() method examined earlier (see Listing 21.31), emitProfileForm() uses a for loop to step through each element of the mProfileFields fieldmap array.

Listing 21.33 The SimpleSiteServlet emitProfileForm() Method
 366. 367.     /* 368.      * emitProfileForm(): common code for "editprofile" and "newprofile" 369.      *     HTTP GET subrequests. 370.      */ 371.     private void emitProfileForm( 372.             Attributes attrs,   // if null, create a "new profile" form 373.             PrintWriter writer ) 374.             throws NamingException { 375. 376.         boolean newProfile = ( attrs == null ); 377. 378.         writer.println( "<form action=\"saveprofile\" method=\"POST\">" ); 379.         writer.println( "<table border=\"0\">\n<tbody>" ); 380.         for ( int i = 0; i < mProfileFields.length; ++i ) { 381.             if ( !newProfile && mProfileFields[i].mNewProfileOnly ) { 382.                 continue; 383.             } 384. 385.             // retrieve the attribute value, if present 386.             Attribute attr = null; 387.             String val = ""; 388. 389.             if ( attrs != null ) { 390.                 attr = attrs.get( mProfileFields[i].mLdapAttrName ); 391.                 if ( attr != null ) { 392.                     val = (String)attr.get(); 393.                 } 394.             } 395. 396.             if ( mProfileFields[i].mLdapAttrName.equalsIgnoreCase( 397.                         "simpleEmailFormat" )) { 398.                 // use radio buttons for preferred e-mail format 399.                 String  textChecked, htmlChecked; 400. 401.                 if ( val.length() == 0 402.                              val.equalsIgnoreCase( "text/plain" )) { 403.                     textChecked = " CHECKED"; 404.                     htmlChecked = ""; 405.                 } else { 406.                     textChecked = ""; 407.                     htmlChecked = " CHECKED"; 408.                 } 409. 410.                 writer.println( "<tr><td>" 411.                         + mProfileFields[i].mHtmlFieldName 412.                         + ":</td>\n<td>" ); 413.                 writer.println( "<input type=\"radio\" name=\"" 414.                         + mProfileFields[i].mLdapAttrName 415.                         + "\" value=\"text/plain\"" 416.                         + textChecked + ">&nbsp;Text<br>" ); 417.                 writer.println( "<input type=\"radio\" name=\"" 418.                         + mProfileFields[i].mLdapAttrName 419.                         + "\" value=\"text/html\"" 420.                         + htmlChecked + ">&nbsp;HTML</td></tr>" ); 421.             } else { 422.                 String fieldType = "text"; 423. 424.                 if ( mProfileFields[i].mLdapAttrName.equalsIgnoreCase( 425.                         "userPassword" )) { 426.                     // use an HTML password form field for userPassword 427.                     fieldType = "password"; 428.                 } 429. 430.                 writer.println( "<tr><td>" 431.                         + mProfileFields[i].mHtmlFieldName 432.                         + ":</td>" ); 433.                 writer.print( "<td><input type=\"" + fieldType + 434.                         "\" name=\"" + 435.                         mProfileFields[i].mLdapAttrName + "\"" ); 436.                 if ( mProfileFields[i].mHtmlFieldSize > 0 ) { 437.                     writer.print( " size=\"" 438.                             + mProfileFields[i].mHtmlFieldSize 439.                             + "\"" ); 440.                 } 441.                 writer.println( " value=\"" + val + "\"></td></tr>" ); 442.             } 443.         } 444. 445.         writer.println( "</tbody></table>" ); 446.         if ( newProfile ) { 447.             writer.println( "<input type=\"hidden\"" + 448.                     " name=\"_NewProfile\" value=\"TRUE\">" ); 449.         } 450.         writer.println( "<input type=\"submit\" value=\"Save\">" ); 451.         writer.println( "<input type=\"button\" value=\"Return to" + 452.             " Previous Page\" onClick=\"window.history.back()\">" ); 453.         writer.println( "</form>" ); 454.         writePageFooter( null, false, writer ); 455.     } 456. 

The code on lines 385 to 394 uses JNDI methods to retrieve the values of the existing attributes. The remainder of the emitProfileForm() code outputs an HTML form by creating a table that contains a series of text labels in the first column and HTML input fields in the second column. The field for the simpleEmailFormat attribute is represented by two radio buttons (one for text/plain and one for text/html ). The field for the userPassword attribute is of type password (to ensure that the characters typed are not echoed to the user's screen), and the other fields are simple text input fields. If an attribute has a value, the value is written out. If the attrs parameter is null (signifying that this is a new profile form) or if no values are retrieved, the input field is written with an empty value. The code on lines 445 to 454 completes the table, includes a hidden HTML form field named _NewProfile if this is a new profile form, and creates the Save and Return to Previous Page buttons.

The only remaining HTTP request handler method is doSaveProfile() , which is called when the user submits a new or modified profile. This method demonstrates how to create a new LDAP entry or modify an existing one using JNDI. Listing 21.34 shows the first part of the doSaveProfile() method.

Listing 21.34 The SimpleSiteServlet doSaveProfile() Method (Part 1 of 2)
 457.     /* 458.      * doSaveProfile(): handle a "saveprofile" HTTP POST subrequest 459.      */ 460.     private void doSaveProfile( // HTTP POST method 461.             HttpServletRequest request, 462.             HttpServletResponse response, 463.             PrintWriter writer ) { 464. 465.         // retrieve the HTTP session 466.         HttpSession httpsession = request.getSession(); 467. 468.         // determine whether we are processing a new profile 469.         boolean newProfile = 470.                 ( request.getParameter( "_NewProfile" ) != null ); 471. 472.         try { 473.             String id, partialDN, newPwd = null; 474.             DirContext ctx; 475. 476.             if ( newProfile ) { 477.                 // generate the ID plus DN and generate start of page 478.                 id = request.getParameter( "mail" ); 479.                 if ( id == null  id.length() == 0 ) { 480.                     reportError( "New profile: email address required", 481.                             null, writer ); 482.                     return; 483.                 } 484.                 partialDN = "mail=" + id + "," + mAddLocation; 485.                 writePageHeader( "Creating Profile for " + id, writer ); 486. 487.                 // establish an authenticated LDAP connection 488.                 ctx = createLDAPContext( mAdder, mAdderPwd ); 489.             } else { 490.                 // retrieve the ID plus DN and generate start of page 491.                 id = getIDWithRedirect( httpsession, response, writer ); 492.                 if ( id == null ) return; 493.                  partialDN = (String)httpsession.getAttribute( "partialDN" ); 494.                 writePageHeader( "Saving Profile for " + id, writer ); 495. 496.                 ctx = createLDAPContext( httpsession ); // authenticate 497.             } 498. 499.             // count the values 500.             int attrCount = 0; 501.             for ( int i = 0; i < mProfileFields.length; ++i ) { 502.                 String ldapAttr = mProfileFields[i].mLdapAttrName; 503.                 String val = request.getParameter( ldapAttr ); 504. 505.                 if ( val != null ) { 506.                     ++attrCount; 507.                     if ( ldapAttr.equalsIgnoreCase( "userPassword" )) { 508.                         newPwd = val; 509.                     } 510.                 } 511.             } 

The code on lines 476 to 497 creates an authenticated JNDI LDAP directory context by calling the createLDAPContext() utility method we examined earlier. There are two cases:

  1. Creating a new profile . A new partial DN is constructed from the e-mail address provided by the user with the mAddLocation value appended. For example, if the mAddLocation value is ou=Members , and the e-mail address provided by the user is bjensen@example.com , then the resulting partial DN will be mail=bjensen@example.com,ou=Members . The directory context is authenticated by the mAdder partial DN and the mAdderPwd password value. The mAdder and mAdderPwd values are configurable values that identify an administrative LDAP entry that has permission to add new entries.

  2. Modifying an existing profile . The directory context is created from the identity information present in the HTTP session.

The code on lines 499 to 511 loops through the elements of the mProfileFields fieldmap array to determine how many attributes were included in the HTTP POST data. The count is used later to create arrays of the right size.

Listing 21.35 shows part two of the doSaveProfile() method. This code adds a new LDAP entry or modifies an existing one.

Listing 21.35 The SimpleSiteServlet doSaveProfile() Method (Part 2 of 2)
 512. 513.             ModificationItem[] mods = null; 514.             BasicAttributes attrs = null; 515. 516.             if ( newProfile ) { 517.                 // create a collection of attributes 518.                 attrs = new BasicAttributes(); 519.                 // include the objectClass values in the set 520.                 BasicAttribute ocattr = new BasicAttribute( "objectClass" ); 521.                 for ( int i = 0; i < mObjectClassValues.length; ++i ) { 522.                     ocattr.add( mObjectClassValues[i] ); 523.                 } 524.                 attrs.put( ocattr ); 525.             } else { 526.                 // create an array of modification items 527.                 mods = new ModificationItem[ attrCount ]; 528.             } 529. 530.             // populate the attrs collection or the mod item array 531.             attrCount = 0; 532.             for ( int i = 0; i < mProfileFields.length; ++i ) { 533.                 String ldapAttr = mProfileFields[i].mLdapAttrName; 534.                 String val = request.getParameter( ldapAttr ); 535. 536.                 if ( val != null ) { 537.                     BasicAttribute attr; 538. 539.                     if ( newProfile && val.length() > 0 ) { 540.                         attr = new BasicAttribute( ldapAttr, val ); 541.                         attrs.put( attr ); 542.                         if ( ldapAttr.equalsIgnoreCase( "cn" )) { 543.                             // construct a surname from the cn (simplistic) 544.                             String snVal; 545.                             int idx = val.indexOf( " " ); 546.                             if ( idx >= 0 ) { 547.                                 snVal = val.substring( idx + 1 ); 548.                             } else { 549.                                 snVal = val; 550.                             } 551.                             attrs.put( new BasicAttribute( "sn", snVal )); 552.                         } 553.                     } else if ( !newProfile ) { 554.                         if ( val.length() > 0 ) { 555.                             attr =  new BasicAttribute( ldapAttr, val ); 556.                         } else {    // remove all values 557.                             attr =  new BasicAttribute( ldapAttr ); 558.                         } 559.                         mods[ attrCount ] = new ModificationItem( 560.                                 ctx.REPLACE_ATTRIBUTE, attr ); 561.                     } 562.                     ++attrCount; 563.                 } 564.             } 565. 566.             // make the change via LDAP 567.             if ( newProfile ) { 568.                 ctx.createSubcontext( partialDN, attrs ); 569.                 // store user information in the HTTP session 570.                 httpsession.setAttribute( "id", id ); 571.                 httpsession.setAttribute( "partialDN", partialDN ); 572.                 httpsession.setAttribute( "pwd", newPwd ); 573.             } else { 574.                 ctx.modifyAttributes( partialDN, mods ); 575.             } 576. 577.             // finish the page and close the LDAP connection 578.             writePageFooter( "<h3>Profile successfully saved.</h3>", 579.                     true, writer ); 580.             ctx.close(); 581.         } catch ( Exception e ) { 582.             reportError( "Edit Profile:", e, writer ); 583.         } 584.     } 

The code on lines 517 to 524 is executed when a new profile is submitted. It creates a new JNDI BasicAttributes object named attrs and adds the object class values required for new entries. The BasicAttributes object is an implementation of the JNDI Attributes interface, which holds a collection of directory attributes.

The code on line 527 is executed when an existing profile is modified. It creates an array of JNDI ModificationItem objects named mods ; each element represents a modification to one LDAP attribute. The for loop that encompasses lines 532 to 564 uses the posted HTML form data to populate the attrs object or the mods array, as appropriate.

For new profiles, a BasicAttribute object is created and added to the attrs collection for each field (lines 540 and 541). The code on lines 542 to 551 creates a surname ( sn ) attribute based on the common name ( cn ) attribute (the sn attribute is never exposed to SimpleSite users). The LDAP add operation itself is performed by the createSubcontext() call on line 568. The terminology used by JNDI does not always match that used by LDAP practitioners ; in JNDI a subcontext is simply an LDAP entry that is located beneath the base DN that was used to create the initial directory context. The base DN used to create the initial directory context is stored in the mBaseDN SimpleSiteServlet member variable and is also part of the mLdapURL value. For example, if the partialDN value is mail=mcs@netscape.com, ou=Members and the mBaseDN value is dc=simple,dc=example,dc=com , then the entry created will have a full DN of mail=mcs@netscape.com,ou=Members, dc=simple,dc=example,dc=com .

For updates to existing profiles, an LDAP modification type that specifies a replace operation is always used within each mods array element (lines 559 and 560). The LDAP modify operation is performed by the JNDI modifyAttributes() call on line 574; this time, the purpose of the method is clear from its name.

Listing 21.36 shows three utility methods that create HTML output. These functions do not perform any LDAP-related work; therefore, they are not described in detail.

Listing 21.36 The SimpleSiteServlet writePageHeader() , writePageFooter() , and writeHREFButton() Methods
 788. 789.     /* 790.      * writePageHeader(): Output an HTML page header with title. 791.      */ 792.     private void writePageHeader( 793.             String title, 794.             PrintWriter writer ) { 795. 796.         if ( title == null ) { 797.             title = mApplName; 798.         } 799.         writer.println( "<html>" ); 800.         writer.println( "<head><title>" + title + "</title></head>" ); 801.         writer.println( "<body>\n<center><h2>" + title + "</h2></center>" ); 802.     } 803. 804.     /* 805.      * writePageFooter(): Output an HTML page footer. 806.      */ 807.     private void writePageFooter( 808.             String statusMessage, 809.             boolean displayActions, 810.             PrintWriter writer ) { 811. 812.         if ( statusMessage != null ) { 813.             writer.println( "<p>" + statusMessage ); 814.         } 815.         if ( displayActions ) { 816.             writer.println( "<form>" ); 817.             writeHREFButton( "Find Person", "../find.htm", writer ); 818.             writeHREFButton( "Edit Profile", "editprofile", writer ); 819.             writeHREFButton( "Log Out ", "logout", writer ); 820.             writer.println( "</form>" ); 821.         } 822. 823.         writer.println( "</body>\n</html>" ); 824.     } 825. 826.     /* 827.      * writeHREFButton(): Output a button that is a link. 828.      */ 829.     private void writeHREFButton( 830.             String label, 831.             String url, 832.             PrintWriter writer ) { 833. 834.         writer.println( "<input type=\"button\" value=\"" 835.                 + label + "\" onClick=\"window.location.href='" 836.                 + url + "'\">&nbsp;&nbsp;" ); 837.     } 838. 

Listing 21.37 shows the last two methods that make up the SimpleSiteServlet class: unknownRequest() and reportError() . These two methods are used to report errors to the user in a consistent way.

Listing 21.37 The SimpleSiteServlet unknownRequest() and reportError() Methods
 839. 840.     /* 841.      * unknownRequest(): Output an "unknownRequest" HTML error 842.      *    page (entire page). 843.      */ 844.     private void unknownRequest( 845.             String msg, 846.             String type, // get, post, ... 847.             PrintWriter writer ) { 848. 849.         writePageHeader( mApplName + " - unknown request", writer ); 850.         reportError( "Unknown " + type + " request: (" + msg + ")", 851.                 null, writer ); 852.     } 853. 854.     /* 855.      * reportError(): Output an HTML error page (all but page header). 856.      */ 857.     private void reportError( 858.             String msg, 859.             Exception e, 860.             PrintWriter writer ) { 861. 862.         writer.println( "<p><b>" + msg + "</b><br>" ); 863.         if ( e != null ) { 864.             writer.println( "Error: " + e + "<br>" ); 865.         } 866.         writer.println( "<form><input type=\"button\" " 867.                 + "value=\"Try Again\" " 868.                 + "onClick=\"window.history.back()\"></form>" ); 869.         writePageFooter( null, false, writer ); 870.     } 871. } 

Although the SimpleSiteServlet.java file includes fewer than 900 lines of code, it does a lot: It provides a complete, authenticated Web site with user profile storage and a search capability.

Ideas for Improvement

The SimpleSite application could be improved in many ways. Here are a few ideas:

  • Separate the HTML page content from the programming logic. Although it is simpler for beginners to generate HTML code directly from Java methods, use of JSP or another template-based approach is highly recommended. That way, Web designers can improve the appearance of the application without requiring Java source code changes.

  • Improve the error reporting and handling. The code makes very little effort to present JNDI exceptions in a user-friendly way.

  • Add a Confirm Password field to the Create New Profile form to protect users against simple typographic errors when entering their passwords.

  • Add a separate field for surname (rather than programmatically extracting an sn value from the name provided by the user).

  • Reduce the update load on the directory server by detecting and omitting the profile values that have no changes from the list of modifications sent to the server.

  • Support more than one value within some of the user profile fields. For example, the simpleExpertise attribute typically has more than one value from the user's perspective, but it is stored as a single free-form string value.

  • Add additional search capabilities. Either/or searching, as well as substring and approximate matching for fields such as Name , could be supported.

   


Understanding and Deploying LDAP Directory Services
Understanding and Deploying LDAP Directory Services (2nd Edition)
ISBN: 0672323168
EAN: 2147483647
Year: 2002
Pages: 242

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net