Understanding and Deploying LDAP Directory Services, Second Edition [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Understanding and Deploying LDAP Directory Services, Second Edition [Electronic resources] - نسخه متنی

Timothy A. Howes, Mark C. Smith, and Gordon S. Good

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید








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 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 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 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 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 2037) 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 userfor example, mcs@netscape.com.




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




  3. pwd .
    The LDAP password of the logged-in userfor example, secret.





The doLogin() method sets the session attributes (lines 206209). 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 646654 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 escapednamely, *, (, ), 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 784786), 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" );
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", 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.





/ 241