1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package net.sf.ldaptemplate.support;
17
18 import java.util.Hashtable;
19 import java.util.Map;
20
21 import javax.naming.Context;
22 import javax.naming.NamingException;
23 import javax.naming.directory.DirContext;
24
25 import org.apache.commons.lang.ArrayUtils;
26 import org.apache.commons.lang.StringUtils;
27 import org.apache.commons.logging.Log;
28 import org.apache.commons.logging.LogFactory;
29 import org.springframework.beans.factory.InitializingBean;
30 import org.springframework.core.JdkVersion;
31
32 import net.sf.ldaptemplate.AuthenticationSource;
33 import net.sf.ldaptemplate.ContextSource;
34 import net.sf.ldaptemplate.DefaultNamingExceptionTranslator;
35 import net.sf.ldaptemplate.NamingExceptionTranslator;
36
37 /***
38 * Abstract implementation of the ContextSource interface. By default, returns
39 * an unauthenticated DirContext implementation for read-only purposes and an
40 * authenticated one for read-write.
41 * <p>
42 * Implementing classes need to implement
43 * {@link #getDirContextInstance(Hashtable)} to create a DirContext instance of
44 * the desired type.
45 * <p>
46 * If an AuthenticationSource is set, this will be used for getting user name
47 * and password for each new connection, otherwise a default one will be created
48 * using the specified userName and password. To use an authenticated
49 * environment for read-only access as well (which is commonly needed when
50 * working against Active Directory) set authenticatedReadOnly to
51 * <code>true</code>.
52 * <p>
53 * <b>Note:</b> When using implementations of this class outside of a Spring
54 * Context it is necessary to call {@link #afterPropertiesSet()} when all
55 * properties are set, in order to finish up initialization.
56 *
57 * @see net.sf.ldaptemplate.LdapTemplate
58 * @see net.sf.ldaptemplate.support.DefaultDirObjectFactory
59 * @see net.sf.ldaptemplate.support.LdapContextSource
60 * @see net.sf.ldaptemplate.support.DirContextSource
61 *
62 * @author Mattias Arthursson
63 * @author Adam Skogman
64 * @author Ulrik Sandberg
65 */
66 public abstract class AbstractContextSource implements ContextSource,
67 InitializingBean {
68
69 private static final Class DEFAULT_CONTEXT_FACTORY = com.sun.jndi.ldap.LdapCtxFactory.class;
70
71 private Class dirObjectFactory;
72
73 private Class contextFactory = DEFAULT_CONTEXT_FACTORY;
74
75 private DistinguishedName base;
76
77 protected String userName = "";
78
79 protected String password = "";
80
81 private String[] urls;
82
83 private boolean pooled = true;
84
85 private boolean authenticatedReadOnly = false;
86
87 private Hashtable baseEnv = new Hashtable();
88
89 private Hashtable anonymousEnv;
90
91 private AuthenticationSource authenticationSource;
92
93 private boolean cacheEnvironmentProperties = true;
94
95 private NamingExceptionTranslator exceptionTranslator = new DefaultNamingExceptionTranslator();
96
97 private static final Log log = LogFactory.getLog(LdapContextSource.class);
98
99 public static final String SUN_LDAP_POOLING_FLAG = "com.sun.jndi.ldap.connect.pool";
100
101 private static final String JDK_142 = "1.4.2";
102
103 public DirContext getReadOnlyContext() {
104 if (authenticatedReadOnly) {
105 return createContext(getAuthenticatedEnv());
106 } else {
107 return createContext(getAnonymousEnv());
108 }
109 }
110
111 public DirContext getReadWriteContext() {
112 return createContext(getAuthenticatedEnv());
113 }
114
115 /***
116 * Default implementation of setting the environment up to be authenticated.
117 * Override in subclass if necessary, e.g. for Active Directory
118 * connectivity.
119 *
120 * @param env
121 * the environment to modify.
122 */
123 protected void setupAuthenticatedEnvironment(Hashtable env) {
124 env
125 .put(Context.SECURITY_PRINCIPAL, authenticationSource
126 .getPrincipal());
127 log.debug("Principal: '" + userName + "'");
128 env.put(Context.SECURITY_CREDENTIALS, authenticationSource
129 .getCredentials());
130 }
131
132 /***
133 * Close the context and swallow any exceptions.
134 *
135 * @param ctx
136 * the DirContext to close.
137 */
138 private void closeContext(DirContext ctx) {
139 if (ctx != null) {
140 try {
141 ctx.close();
142 } catch (Exception e) {
143 }
144 }
145 }
146
147 /***
148 * Assemble a valid url String from all registered urls to add as
149 * PROVIDER_URL to the environment.
150 *
151 * @param ldapUrls
152 * all individual url Strings.
153 * @return the full url String
154 */
155 protected String assembleProviderUrlString(String[] ldapUrls) {
156 StringBuffer providerUrlBuffer = new StringBuffer(1024);
157 for (int i = 0; i < ldapUrls.length; i++) {
158 providerUrlBuffer.append(ldapUrls[i]);
159 if (base != null) {
160 if (!ldapUrls[i].endsWith("/")) {
161 providerUrlBuffer.append("/");
162 }
163 providerUrlBuffer.append(base.toUrl());
164 }
165 providerUrlBuffer.append(' ');
166 }
167 return providerUrlBuffer.toString().trim();
168 }
169
170 /***
171 * Set the base suffix from which all operations should origin. If a base
172 * suffix is set, you will not have to (and, indeed, should not) specify the
173 * full distinguished names in the operations performed.
174 *
175 * @param base
176 * the base suffix.
177 */
178 public void setBase(String base) {
179 this.base = new DistinguishedName(base);
180 }
181
182 /***
183 * Create a DirContext using the supplied environment.
184 *
185 * @param environment
186 * the Ldap environment to use when creating the DirContext.
187 * @return a new DirContext implpementation initialized with the supplied
188 * environment.
189 */
190 DirContext createContext(Hashtable environment) {
191 DirContext ctx = null;
192
193 try {
194 ctx = getDirContextInstance(environment);
195
196 if (log.isInfoEnabled()) {
197 Hashtable ctxEnv = ctx.getEnvironment();
198 String ldapUrl = (String) ctxEnv.get(Context.PROVIDER_URL);
199 log.debug("Got Ldap context on server '" + ldapUrl + "'");
200 }
201
202 return ctx;
203 } catch (NamingException e) {
204 closeContext(ctx);
205 throw getExceptionTranslator().translate(e);
206 }
207 }
208
209 /***
210 * Set the context factory. Default is com.sun.jndi.ldap.LdapCtxFactory.
211 *
212 * @param contextFactory
213 * the context factory used when creating Contexts.
214 */
215 public void setContextFactory(Class contextFactory) {
216 this.contextFactory = contextFactory;
217 }
218
219 /***
220 * Set the DirObjectFactory to use. Default is null - no DirObjectFactory is
221 * used. The specified class needs to be an implementation of
222 * javax.naming.spi.DirObjectFactory, e.g. {@link DefaultDirObjectFactory}.
223 *
224 * @param dirObjectFactory
225 * the DirObjectFactory to be used. Null means that no
226 * DirObjectFactory will be used.
227 */
228 public void setDirObjectFactory(Class dirObjectFactory) {
229 this.dirObjectFactory = dirObjectFactory;
230 }
231
232 /***
233 * Checks that all necessary data is set and that there is no compatibility
234 * issues, after which the instance is initialized. Note that you need to
235 * call this method explicitly after setting all desired properties if using
236 * the class outside of a Spring Context.
237 */
238 public void afterPropertiesSet() throws Exception {
239 if (ArrayUtils.isEmpty(urls)) {
240 throw new IllegalArgumentException(
241 "At least one server url must be set");
242 }
243
244 if (base != null && getJdkVersion().compareTo(JDK_142) <= 0) {
245 throw new IllegalArgumentException(
246 "Base path is not supported for JDK versions < 1.4.2");
247 }
248
249 if (authenticationSource == null) {
250 log.debug("AuthenticationSource not set - "
251 + "using default implementation");
252 if (StringUtils.isBlank(userName)) {
253 log
254 .warn("Property 'userName' not set - "
255 + "anonymous context will be used for read-write operations");
256 } else if (StringUtils.isBlank(password)) {
257 log.warn("Property 'password' not set - "
258 + "blank password will be used");
259 }
260 authenticationSource = new SimpleAuthenticationSource();
261 }
262
263 if (cacheEnvironmentProperties) {
264 anonymousEnv = setupAnonymousEnv();
265 }
266 }
267
268 private Hashtable setupAnonymousEnv() {
269 if (pooled) {
270 baseEnv.put(SUN_LDAP_POOLING_FLAG, "true");
271 log.debug("Using LDAP pooling.");
272 } else {
273 log.debug("Not using LDAP pooling");
274 }
275
276 Hashtable env = new Hashtable(baseEnv);
277
278 env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory.getName());
279 env.put(Context.PROVIDER_URL, assembleProviderUrlString(urls));
280
281 if (dirObjectFactory != null) {
282 env.put(Context.OBJECT_FACTORIES, dirObjectFactory.getName());
283 }
284
285 if (base != null) {
286
287 env.put(DefaultDirObjectFactory.JNDI_ENV_BASE_PATH_KEY, base);
288 }
289
290 log.debug("Trying provider Urls: " + assembleProviderUrlString(urls));
291
292 return env;
293 }
294
295 /***
296 * Set the password (credentials) to use for getting authenticated contexts.
297 *
298 * @param password
299 * the password.
300 */
301 public void setPassword(String password) {
302 this.password = password;
303 }
304
305 /***
306 * Set the user name (principal) to use for getting authenticated contexts.
307 *
308 * @param userName
309 * the user name.
310 */
311 public void setUserName(String userName) {
312 this.userName = userName;
313 }
314
315 /***
316 * Set the urls of the LDAP servers. Use this method if several servers are
317 * required.
318 *
319 * @param urls
320 * the urls of all servers.
321 */
322 public void setUrls(String[] urls) {
323 this.urls = urls;
324 }
325
326 /***
327 * Set the url of the LDAP server. Utility method if only one server is
328 * used.
329 *
330 * @param url
331 * the url of the LDAP server.
332 */
333 public void setUrl(String url) {
334 this.urls = new String[] { url };
335 }
336
337 /***
338 * Set whether the pooling flag should be set. Default is true.
339 *
340 * @param pooled
341 * whether Contexts should be pooled.
342 */
343 public void setPooled(boolean pooled) {
344 this.pooled = pooled;
345 }
346
347 /***
348 * If any custom environment properties are needed, these can be set using
349 * this method.
350 *
351 * @param baseEnvironmentProperties
352 */
353 public void setBaseEnvironmentProperties(Map baseEnvironmentProperties) {
354 this.baseEnv = new Hashtable(baseEnvironmentProperties);
355 }
356
357 String getJdkVersion() {
358 return JdkVersion.getJavaVersion();
359 }
360
361 protected Hashtable getAnonymousEnv() {
362 if (cacheEnvironmentProperties) {
363 return anonymousEnv;
364 } else {
365 return setupAnonymousEnv();
366 }
367 }
368
369 protected Hashtable getAuthenticatedEnv() {
370
371 Hashtable env = new Hashtable(getAnonymousEnv());
372 setupAuthenticatedEnvironment(env);
373 return env;
374 }
375
376 /***
377 * Set to true if an authenticated environment should be created for
378 * read-only operations. Default is <code>false</code>.
379 *
380 * @param authenticatedReadOnly
381 */
382 public void setAuthenticatedReadOnly(boolean authenticatedReadOnly) {
383 this.authenticatedReadOnly = authenticatedReadOnly;
384 }
385
386 public void setAuthenticationSource(
387 AuthenticationSource authenticationProvider) {
388 this.authenticationSource = authenticationProvider;
389 }
390
391 /***
392 * Set whether environment properties should be cached between requsts for
393 * anonymous environment. Default is true; setting this property to false
394 * causes the environment Hashmap to be rebuilt from the current property
395 * settings of this instance between each request for an anonymous
396 * environment.
397 *
398 * @param cacheEnvironmentProperties
399 * true causes that the anonymous environment properties should
400 * be cached, false causes the Hashmap to be rebuilt for each
401 * request.
402 */
403 public void setCacheEnvironmentProperties(boolean cacheEnvironmentProperties) {
404 this.cacheEnvironmentProperties = cacheEnvironmentProperties;
405 }
406
407 /***
408 * Set the NamingExceptionTranslator to be used by this instance. By
409 * default, a {@link DefaultNamingExceptionTranslator} will be used.
410 *
411 * @param exceptionTranslator
412 * the NamingExceptionTranslator to use.
413 */
414 public void setExceptionTranslator(
415 NamingExceptionTranslator exceptionTranslator) {
416 this.exceptionTranslator = exceptionTranslator;
417 }
418
419 /***
420 * Get the NamingExceptionTranslator used by this instance.
421 *
422 * @return the NamingExceptionTranslator.
423 */
424 public NamingExceptionTranslator getExceptionTranslator() {
425 return exceptionTranslator;
426 }
427
428 /***
429 * Implement in subclass to create a DirContext of the desired type (e.g.
430 * InitialDirContext or InitialLdapContext).
431 *
432 * @param environment
433 * the environment to use when creating the instance.
434 * @return a new DirContext instance.
435 * @throws NamingException
436 * if one is encountered when creating the instance.
437 */
438 protected abstract DirContext getDirContextInstance(Hashtable environment)
439 throws NamingException;
440
441 class SimpleAuthenticationSource implements AuthenticationSource {
442
443 public String getPrincipal() {
444 return userName;
445 }
446
447 public String getCredentials() {
448 return password;
449 }
450
451 }
452 }