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