View Javadoc

1   /*
2    * Copyright 2002-2005 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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             // Save the base path for use in the DefaultDirObjectFactory.
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         // The authenticated environment should always be rebuilt.
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 }