001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2008 Sun Microsystems, Inc.
026     */
027    
028    package org.opends.admin.ads.util;
029    
030    import java.io.IOException;
031    import java.net.ConnectException;
032    import java.net.URI;
033    import java.security.GeneralSecurityException;
034    import java.util.HashSet;
035    import java.util.Hashtable;
036    import java.util.Set;
037    import java.util.logging.Level;
038    import java.util.logging.Logger;
039    
040    import javax.naming.CommunicationException;
041    import javax.naming.Context;
042    import javax.naming.NamingException;
043    import javax.naming.directory.Attribute;
044    import javax.naming.directory.Attributes;
045    import javax.naming.directory.SearchControls;
046    import javax.naming.directory.SearchResult;
047    import javax.naming.ldap.InitialLdapContext;
048    import javax.naming.ldap.StartTlsRequest;
049    import javax.naming.ldap.StartTlsResponse;
050    import javax.net.ssl.HostnameVerifier;
051    import javax.net.ssl.KeyManager;
052    import javax.net.ssl.SSLHandshakeException;
053    import javax.net.ssl.TrustManager;
054    
055    /**
056     * Class providing some utilities to create LDAP connections using JNDI and
057     * to manage entries retrieved using JNDI.
058     *
059     */
060    public class ConnectionUtils
061    {
062      private static final int DEFAULT_LDAP_CONNECT_TIMEOUT = 30000;
063    
064      private static final String STARTTLS_PROPERTY =
065        "org.opends.connectionutils.isstarttls";
066    
067      static private final Logger LOG =
068        Logger.getLogger(ConnectionUtils.class.getName());
069    
070      /**
071       * Private constructor: this class cannot be instantiated.
072       */
073      private ConnectionUtils()
074      {
075      }
076    
077      /**
078       * Creates a clear LDAP connection and returns the corresponding LdapContext.
079       * This methods uses the specified parameters to create a JNDI environment
080       * hashtable and creates an InitialLdapContext instance.
081       *
082       * @param ldapURL
083       *          the target LDAP URL
084       * @param dn
085       *          passed as Context.SECURITY_PRINCIPAL if not null
086       * @param pwd
087       *          passed as Context.SECURITY_CREDENTIALS if not null
088       * @param timeout
089       *          passed as com.sun.jndi.ldap.connect.timeout if > 0
090       * @param env
091       *          null or additional environment properties
092       *
093       * @throws NamingException
094       *           the exception thrown when instantiating InitialLdapContext
095       *
096       * @return the created InitialLdapContext.
097       * @see javax.naming.Context
098       * @see javax.naming.ldap.InitialLdapContext
099       */
100      public static InitialLdapContext createLdapContext(String ldapURL, String dn,
101          String pwd, int timeout, Hashtable<String, String> env)
102          throws NamingException
103      {
104        if (env != null)
105        { // We clone 'env' so that we can modify it freely
106          env = new Hashtable<String, String>(env);
107        } else
108        {
109          env = new Hashtable<String, String>();
110        }
111        env.put(Context.INITIAL_CONTEXT_FACTORY,
112            "com.sun.jndi.ldap.LdapCtxFactory");
113        env.put(Context.PROVIDER_URL, ldapURL);
114        if (timeout >= 1)
115        {
116          env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(timeout));
117        }
118        if (dn != null)
119        {
120          env.put(Context.SECURITY_PRINCIPAL, dn);
121        }
122        if (pwd != null)
123        {
124          env.put(Context.SECURITY_CREDENTIALS, pwd);
125        }
126    
127        /* Contains the DirContext and the Exception if any */
128        final Object[] pair = new Object[]
129          { null, null };
130        final Hashtable fEnv = env;
131        Thread t = new Thread(new Runnable()
132        {
133          public void run()
134          {
135            try
136            {
137              pair[0] = new InitialLdapContext(fEnv, null);
138    
139            } catch (NamingException ne)
140            {
141              pair[1] = ne;
142    
143            } catch (Throwable t)
144            {
145              t.printStackTrace();
146              pair[1] = t;
147            }
148          }
149        });
150        return getInitialLdapContext(t, pair, timeout);
151      }
152    
153      /**
154       * Creates an LDAPS connection and returns the corresponding LdapContext.
155       * This method uses the TrusteSocketFactory class so that the specified
156       * trust manager gets called during the SSL handshake. If trust manager is
157       * null, certificates are not verified during SSL handshake.
158       *
159       * @param ldapsURL      the target *LDAPS* URL.
160       * @param dn            passed as Context.SECURITY_PRINCIPAL if not null.
161       * @param pwd           passed as Context.SECURITY_CREDENTIALS if not null.
162       * @param timeout       passed as com.sun.jndi.ldap.connect.timeout if > 0.
163       * @param env           null or additional environment properties.
164       * @param trustManager  null or the trust manager to be invoked during SSL
165       * negociation.
166       * @param keyManager    null or the key manager to be invoked during SSL
167       * negociation.
168       * @return the established connection with the given parameters.
169       *
170       * @throws NamingException the exception thrown when instantiating
171       * InitialLdapContext.
172       *
173       * @see javax.naming.Context
174       * @see javax.naming.ldap.InitialLdapContext
175       * @see TrustedSocketFactory
176       */
177      public static InitialLdapContext createLdapsContext(String ldapsURL,
178          String dn, String pwd, int timeout, Hashtable<String, String> env,
179          TrustManager trustManager, KeyManager keyManager) throws NamingException {
180        if (env != null)
181        { // We clone 'env' so that we can modify it freely
182          env = new Hashtable<String, String>(env);
183        } else
184        {
185          env = new Hashtable<String, String>();
186        }
187        env.put(Context.INITIAL_CONTEXT_FACTORY,
188            "com.sun.jndi.ldap.LdapCtxFactory");
189        env.put(Context.PROVIDER_URL, ldapsURL);
190        env.put("java.naming.ldap.factory.socket",
191            org.opends.admin.ads.util.TrustedSocketFactory.class.getName());
192    
193        if (dn != null)
194        {
195          env.put(Context.SECURITY_PRINCIPAL, dn);
196        }
197    
198        if (pwd != null)
199        {
200          env.put(Context.SECURITY_CREDENTIALS, pwd);
201        }
202    
203        if (trustManager == null)
204        {
205          trustManager = new BlindTrustManager();
206        }
207    
208        /* Contains the DirContext and the Exception if any */
209        final Object[] pair = new Object[] {null, null};
210        final Hashtable fEnv = env;
211        final TrustManager fTrustManager = trustManager;
212        final KeyManager   fKeyManager   = keyManager;
213    
214        Thread t = new Thread(new Runnable() {
215          public void run() {
216            try {
217              TrustedSocketFactory.setCurrentThreadTrustManager(fTrustManager,
218                  fKeyManager);
219              pair[0] = new InitialLdapContext(fEnv, null);
220    
221            } catch (NamingException ne) {
222              pair[1] = ne;
223    
224            } catch (RuntimeException re) {
225              pair[1] = re;
226            }
227          }
228        });
229        return getInitialLdapContext(t, pair, timeout);
230      }
231    
232      /**
233       * Creates an LDAP+StartTLS connection and returns the corresponding
234       * LdapContext.
235       * This method first creates an LdapContext with anonymous bind. Then it
236       * requests a StartTlsRequest extended operation. The StartTlsResponse is
237       * setup with the specified hostname verifier. Negotiation is done using a
238       * TrustSocketFactory so that the specified TrustManager gets called during
239       * the SSL handshake.
240       * If trust manager is null, certificates are not checked during SSL
241       * handshake.
242       *
243       * @param ldapURL       the target *LDAP* URL.
244       * @param dn            passed as Context.SECURITY_PRINCIPAL if not null.
245       * @param pwd           passed as Context.SECURITY_CREDENTIALS if not null.
246       * @param timeout       passed as com.sun.jndi.ldap.connect.timeout if > 0.
247       * @param env           null or additional environment properties.
248       * @param trustManager  null or the trust manager to be invoked during SSL
249       * negociation.
250       * @param keyManager    null or the key manager to be invoked during SSL
251       * negociation.
252       * @param verifier      null or the hostname verifier to be setup in the
253       * StartTlsResponse.
254       * @return the established connection with the given parameters.
255       *
256       * @throws NamingException the exception thrown when instantiating
257       * InitialLdapContext.
258       *
259       * @see javax.naming.Context
260       * @see javax.naming.ldap.InitialLdapContext
261       * @see javax.naming.ldap.StartTlsRequest
262       * @see javax.naming.ldap.StartTlsResponse
263       * @see TrustedSocketFactory
264       */
265    
266      public static InitialLdapContext createStartTLSContext(String ldapURL,
267          String dn, String pwd, int timeout, Hashtable<String, String> env,
268          TrustManager trustManager, KeyManager keyManager,
269          HostnameVerifier verifier)
270      throws NamingException
271      {
272        if (trustManager == null)
273        {
274          trustManager = new BlindTrustManager();
275        }
276        if (verifier == null) {
277          verifier = new BlindHostnameVerifier();
278        }
279    
280        if (env != null)
281        { // We clone 'env' to modify it freely
282          env = new Hashtable<String, String>(env);
283        }
284        else
285        {
286          env = new Hashtable<String, String>();
287        }
288        env.put(Context.INITIAL_CONTEXT_FACTORY,
289            "com.sun.jndi.ldap.LdapCtxFactory");
290        env.put(Context.PROVIDER_URL, ldapURL);
291        env.put(Context.SECURITY_AUTHENTICATION , "none");
292    
293        /* Contains the DirContext and the Exception if any */
294        final Object[] pair = new Object[] {null, null};
295        final Hashtable fEnv = env;
296        final String fDn = dn;
297        final String fPwd = pwd;
298        final TrustManager fTrustManager = trustManager;
299        final KeyManager fKeyManager     = keyManager;
300        final HostnameVerifier fVerifier = verifier;
301    
302        Thread t = new Thread(new Runnable() {
303          public void run() {
304            try {
305              StartTlsResponse tls;
306    
307              InitialLdapContext result = new InitialLdapContext(fEnv, null);
308    
309              tls = (StartTlsResponse) result.extendedOperation(
310                  new StartTlsRequest());
311              tls.setHostnameVerifier(fVerifier);
312              try
313              {
314                tls.negotiate(new TrustedSocketFactory(fTrustManager,fKeyManager));
315              }
316              catch(IOException x) {
317                NamingException xx;
318                xx = new CommunicationException(
319                    "Failed to negotiate Start TLS operation");
320                xx.initCause(x);
321                result.close();
322                throw xx;
323              }
324    
325              result.addToEnvironment(STARTTLS_PROPERTY, "true");
326              if (fDn != null)
327              {
328    
329                result.addToEnvironment(Context.SECURITY_AUTHENTICATION , "simple");
330                result.addToEnvironment(Context.SECURITY_PRINCIPAL, fDn);
331                if (fPwd != null)
332                {
333                  result.addToEnvironment(Context.SECURITY_CREDENTIALS, fPwd);
334                }
335                result.reconnect(null);
336              }
337              pair[0] = result;
338    
339            } catch (NamingException ne)
340            {
341              pair[1] = ne;
342    
343            } catch (RuntimeException re)
344            {
345              pair[1] = re;
346            }
347          }
348        });
349        return getInitialLdapContext(t, pair, timeout);
350      }
351    
352      /**
353       * Returns the LDAP URL used in the provided InitialLdapContext.
354       * @param ctx the context to analyze.
355       * @return the LDAP URL used in the provided InitialLdapContext.
356       */
357      public static String getLdapUrl(InitialLdapContext ctx)
358      {
359        String s = null;
360        try
361        {
362          s = (String)ctx.getEnvironment().get(Context.PROVIDER_URL);
363        }
364        catch (NamingException ne)
365        {
366          // This is really strange.  Seems like a bug somewhere.
367          LOG.log(Level.WARNING, "Naming exception getting environment of "+ctx,
368              ne);
369        }
370        return s;
371      }
372    
373      /**
374       * Returns the host name used in the provided InitialLdapContext.
375       * @param ctx the context to analyze.
376       * @return the host name used in the provided InitialLdapContext.
377       */
378      public static String getHostName(InitialLdapContext ctx)
379      {
380        String s = null;
381        try
382        {
383          URI ldapURL = new URI(getLdapUrl(ctx));
384          s = ldapURL.getHost();
385        }
386        catch (Throwable t)
387        {
388          // This is really strange.  Seems like a bug somewhere.
389          LOG.log(Level.WARNING, "Error getting host: "+t, t);
390        }
391        return s;
392      }
393    
394      /**
395       * Returns the port number used in the provided InitialLdapContext.
396       * @param ctx the context to analyze.
397       * @return the port number used in the provided InitialLdapContext.
398       */
399      public static int getPort(InitialLdapContext ctx)
400      {
401        int port = -1;
402        try
403        {
404          URI ldapURL = new URI(getLdapUrl(ctx));
405          port = ldapURL.getPort();
406        }
407        catch (Throwable t)
408        {
409          // This is really strange.  Seems like a bug somewhere.
410          LOG.log(Level.WARNING, "Error getting port: "+t, t);
411        }
412        return port;
413      }
414    
415      /**
416       * Returns the host port representation of the server to which this
417       * context is connected.
418       * @param ctx the context to analyze.
419       * @return the host port representation of the server to which this
420       * context is connected.
421       */
422      public static String getHostPort(InitialLdapContext ctx)
423      {
424        return getHostName(ctx)+":"+getPort(ctx);
425      }
426    
427      /**
428       * Returns the bind DN used in the provided InitialLdapContext.
429       * @param ctx the context to analyze.
430       * @return the bind DN used in the provided InitialLdapContext.
431       */
432      public static String getBindDN(InitialLdapContext ctx)
433      {
434        String bindDN = null;
435        try
436        {
437          bindDN = (String)ctx.getEnvironment().get(Context.SECURITY_PRINCIPAL);
438        }
439        catch (NamingException ne)
440        {
441          // This is really strange.  Seems like a bug somewhere.
442          LOG.log(Level.WARNING, "Naming exception getting environment of "+ctx,
443              ne);
444        }
445        return bindDN;
446      }
447    
448      /**
449       * Returns the password used in the provided InitialLdapContext.
450       * @param ctx the context to analyze.
451       * @return the password used in the provided InitialLdapContext.
452       */
453      public static String getBindPassword(InitialLdapContext ctx)
454      {
455        String bindPwd = null;
456        try
457        {
458          bindPwd = (String)ctx.getEnvironment().get(Context.SECURITY_CREDENTIALS);
459        }
460        catch (NamingException ne)
461        {
462          // This is really strange.  Seems like a bug somewhere.
463          LOG.log(Level.WARNING, "Naming exception getting environment of "+ctx,
464              ne);
465        }
466        return bindPwd;
467      }
468    
469      /**
470       * Tells whether we are using SSL in the provided InitialLdapContext.
471       * @param ctx the context to analyze.
472       * @return <CODE>true</CODE> if we are using SSL and <CODE>false</CODE>
473       * otherwise.
474       */
475      public static boolean isSSL(InitialLdapContext ctx)
476      {
477        boolean isSSL = false;
478        String s = null;
479        try
480        {
481          s = getLdapUrl(ctx);
482          isSSL = s.toLowerCase().startsWith("ldaps");
483        }
484        catch (Throwable t)
485        {
486          // This is really strange.  Seems like a bug somewhere.
487          LOG.log(Level.WARNING, "Error getting if is SSL "+t, t);
488        }
489        return isSSL;
490      }
491    
492      /**
493       * Tells whether we are using StartTLS in the provided InitialLdapContext.
494       * @param ctx the context to analyze.
495       * @return <CODE>true</CODE> if we are using StartTLS and <CODE>false</CODE>
496       * otherwise.
497       */
498      public static boolean isStartTLS(InitialLdapContext ctx)
499      {
500        boolean isStartTLS = false;
501        try
502        {
503          isStartTLS = "true".equalsIgnoreCase((String)ctx.getEnvironment().get(
504                STARTTLS_PROPERTY));
505        }
506        catch (NamingException ne)
507        {
508          // This is really strange.  Seems like a bug somewhere.
509          LOG.log(Level.WARNING, "Naming exception getting environment of "+ctx,
510              ne);
511        }
512        return isStartTLS;
513      }
514    
515      /**
516       * Method used to know if we can connect as administrator in a server with a
517       * given password and dn.
518       * @param ldapUrl the ldap URL of the server.
519       * @param dn the dn to be used.
520       * @param pwd the password to be used.
521       * @return <CODE>true</CODE> if we can connect and read the configuration and
522       * <CODE>false</CODE> otherwise.
523       */
524      public static boolean canConnectAsAdministrativeUser(String ldapUrl,
525          String dn, String pwd)
526      {
527        boolean canConnectAsAdministrativeUser = false;
528        try
529        {
530          InitialLdapContext ctx;
531          if (ldapUrl.toLowerCase().startsWith("ldap:"))
532          {
533            ctx = createLdapContext(ldapUrl, dn, pwd, getDefaultLDAPTimeout(),
534                null);
535          }
536          else
537          {
538            ctx = createLdapsContext(ldapUrl, dn, pwd, getDefaultLDAPTimeout(),
539                null, null, null);
540          }
541    
542          canConnectAsAdministrativeUser = connectedAsAdministrativeUser(ctx);
543        } catch (NamingException ne)
544        {
545          // Nothing to do.
546        } catch (Throwable t)
547        {
548          throw new IllegalStateException("Unexpected throwable.", t);
549        }
550        return canConnectAsAdministrativeUser;
551      }
552    
553      /**
554       * Method used to know if we are connected as administrator in a server with a
555       * given InitialLdapContext.
556       * @param ctx the context.
557       * @return <CODE>true</CODE> if we are connected and read the configuration
558       * and <CODE>false</CODE> otherwise.
559       */
560      public static boolean connectedAsAdministrativeUser(InitialLdapContext ctx)
561      {
562        boolean connectedAsAdministrativeUser = false;
563        try
564        {
565          /*
566           * Search for the config to check that it is the directory manager.
567           */
568          SearchControls searchControls = new SearchControls();
569          searchControls.setCountLimit(1);
570          searchControls.setSearchScope(
571              SearchControls. OBJECT_SCOPE);
572          searchControls.setReturningAttributes(
573              new String[] {"dn"});
574          ctx.search("cn=config", "objectclass=*", searchControls);
575    
576          connectedAsAdministrativeUser = true;
577        } catch (NamingException ne)
578        {
579          // Nothing to do.
580        } catch (Throwable t)
581        {
582          throw new IllegalStateException("Unexpected throwable.", t);
583        }
584        return connectedAsAdministrativeUser;
585      }
586    
587      /**
588       * This is just a commodity method used to try to get an InitialLdapContext.
589       * @param t the Thread to be used to create the InitialLdapContext.
590       * @param pair an Object[] array that contains the InitialLdapContext and the
591       * Throwable if any occurred.
592       * @param timeout the timeout.  If we do not get to create the connection
593       * before the timeout a CommunicationException will be thrown.
594       * @return the created InitialLdapContext
595       * @throws NamingException if something goes wrong during the creation.
596       */
597      private static InitialLdapContext getInitialLdapContext(Thread t,
598          Object[] pair, int timeout) throws NamingException
599      {
600        try
601        {
602          if (timeout > 0)
603          {
604            t.start();
605            t.join(timeout);
606          } else
607          {
608            t.run();
609          }
610    
611        } catch (InterruptedException x)
612        {
613          // This might happen for problems in sockets
614          // so it does not necessarily imply a bug
615        }
616    
617        boolean throwException = false;
618    
619        if ((timeout > 0) && t.isAlive())
620        {
621          t.interrupt();
622          try
623          {
624            t.join(2000);
625          } catch (InterruptedException x)
626          {
627            // This might happen for problems in sockets
628            // so it does not necessarily imply a bug
629          }
630          throwException = true;
631        }
632    
633        if ((pair[0] == null) && (pair[1] == null))
634        {
635          throwException = true;
636        }
637    
638        if (throwException)
639        {
640          NamingException xx;
641          ConnectException x = new ConnectException("Connection timed out");
642          xx = new CommunicationException("Connection timed out");
643          xx.initCause(x);
644          throw xx;
645        }
646    
647        if (pair[1] != null)
648        {
649          if (pair[1] instanceof NamingException)
650          {
651            throw (NamingException) pair[1];
652    
653          } else if (pair[1] instanceof RuntimeException)
654          {
655            throw (RuntimeException) pair[1];
656    
657          } else if (pair[1] instanceof Throwable)
658          {
659            throw new IllegalStateException("Unexpected throwable occurred",
660                (Throwable) pair[1]);
661          }
662        }
663        return (InitialLdapContext) pair[0];
664      }
665    
666      /**
667       * Returns the default LDAP timeout in milliseconds when we try to connect to
668       * a server.
669       * @return the default LDAP timeout in milliseconds when we try to connect to
670       * a server.
671       */
672      public static int getDefaultLDAPTimeout()
673      {
674        return DEFAULT_LDAP_CONNECT_TIMEOUT;
675      }
676    
677      /**
678       * Returns the String that can be used to represent a given host name in a
679       * LDAP URL.
680       * This method must be used when we have IPv6 addresses (the address in the
681       * LDAP URL must be enclosed with brackets).
682       * @param host the host name.
683       * @return the String that can be used to represent a given host name in a
684       * LDAP URL.
685       */
686      public static String getHostNameForLdapUrl(String host)
687      {
688        if ((host != null) && host.indexOf(":") != -1)
689        {
690          // Assume an IPv6 address has been specified and adds the brackets
691          // for the URL.
692          host = host.trim();
693          if (!host.startsWith("["))
694          {
695            host = "["+host;
696          }
697          if (!host.endsWith("]"))
698          {
699            host = host + "]";
700          }
701        }
702        return host;
703      }
704    
705      /**
706       * Returns the LDAP URL for the provided parameters.
707       * @param host the host name.
708       * @param port the LDAP port.
709       * @param useSSL whether to use SSL or not.
710       * @return the LDAP URL for the provided parameters.
711       */
712      public static String getLDAPUrl(String host, int port, boolean useSSL)
713      {
714        String ldapUrl;
715        host = getHostNameForLdapUrl(host);
716        if (useSSL)
717        {
718          ldapUrl = "ldaps://"+host+":"+port;
719        }
720        else
721        {
722          ldapUrl = "ldap://"+host+":"+port;
723        }
724        return ldapUrl;
725      }
726    
727      /**
728       * Tells whether the provided Throwable was caused because of a problem with
729       * a certificate while trying to establish a connection.
730       * @param t the Throwable to analyze.
731       * @return <CODE>true</CODE> if the provided Throwable was caused because of a
732       * problem with a certificate while trying to establish a connection and
733       * <CODE>false</CODE> otherwise.
734       */
735      public static boolean isCertificateException(Throwable t)
736      {
737        boolean returnValue = false;
738    
739        while (!returnValue && (t != null))
740        {
741          returnValue = (t instanceof SSLHandshakeException) ||
742          (t instanceof GeneralSecurityException);
743          t = t.getCause();
744        }
745    
746        return returnValue;
747      }
748    
749      /**
750       * Returns the String representation of the first value of an attribute in a
751       * LDAP entry.
752       * @param entry the entry.
753       * @param attrName the attribute name.
754       * @return the String representation of the first value of an attribute in a
755       * LDAP entry.
756       * @throws NamingException if there is an error processing the entry.
757       */
758      static public String getFirstValue(SearchResult entry, String attrName)
759      throws NamingException
760      {
761        String v = null;
762        Attributes attrs = entry.getAttributes();
763        if (attrs != null)
764        {
765          Attribute attr = attrs.get(attrName);
766          if ((attr != null) && (attr.size() > 0))
767          {
768            Object o = attr.get();
769            if (o instanceof String)
770            {
771              v = (String)o;
772            }
773            else
774            {
775              v = String.valueOf(o);
776            }
777          }
778        }
779        return v;
780      }
781    
782      /**
783       * Returns a Set with the String representation of the values of an attribute
784       * in a LDAP entry.  The returned Set will never be null.
785       * @param entry the entry.
786       * @param attrName the attribute name.
787       * @return a Set with the String representation of the values of an attribute
788       * in a LDAP entry.
789       * @throws NamingException if there is an error processing the entry.
790       */
791      static public Set<String> getValues(SearchResult entry, String attrName)
792      throws NamingException
793      {
794        Set<String> values = new HashSet<String>();
795        Attributes attrs = entry.getAttributes();
796        if (attrs != null)
797        {
798          Attribute attr = attrs.get(attrName);
799          if (attr != null)
800          {
801            for (int i=0; i<attr.size(); i++)
802            {
803              values.add((String)attr.get(i));
804            }
805          }
806        }
807        return values;
808      }
809    }