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