edu.vt.middleware.ldap.AbstractLdap.java Source code

Java tutorial

Introduction

Here is the source code for edu.vt.middleware.ldap.AbstractLdap.java

Source

/*
  $Id$
    
  Copyright (C) 2003-2010 Virginia Tech.
  All rights reserved.
    
  SEE LICENSE FOR MORE INFORMATION
    
  Author:  Middleware Services
  Email:   middleware@vt.edu
  Version: $Revision$
  Updated: $Date$
*/
package edu.vt.middleware.ldap;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.naming.Binding;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import edu.vt.middleware.ldap.handler.AttributeHandler;
import edu.vt.middleware.ldap.handler.AttributesProcessor;
import edu.vt.middleware.ldap.handler.ConnectionHandler;
import edu.vt.middleware.ldap.handler.CopyResultHandler;
import edu.vt.middleware.ldap.handler.SearchCriteria;
import edu.vt.middleware.ldap.handler.SearchResultHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <code>AbstractLdap</code> contains the functions for basic interaction with a
 * LDAP. Methods are provided for connecting, binding, querying and updating.
 *
 * @param  <T>  type of LdapConfig
 *
 * @author  Middleware Services
 * @version  $Revision$ $Date$
 */
public abstract class AbstractLdap<T extends LdapConfig> implements BaseLdap {

    /** Default copy search result handler, used if none supplied. */
    protected static final CopyResultHandler<SearchResult> SR_COPY_RESULT_HANDLER = new CopyResultHandler<SearchResult>();

    /** Default copy name class pair handler. */
    protected static final CopyResultHandler<NameClassPair> NCP_COPY_RESULT_HANDLER = new CopyResultHandler<NameClassPair>();

    /** Default copy binding handler. */
    protected static final CopyResultHandler<Binding> BINDING_COPY_RESULT_HANDLER = new CopyResultHandler<Binding>();

    /** Default copy result handler. */
    protected static final CopyResultHandler<Object> COPY_RESULT_HANDLER = new CopyResultHandler<Object>();

    /** Log for this class. */
    protected final Log logger = LogFactory.getLog(this.getClass());

    /** LDAP connection handler. */
    protected ConnectionHandler connectionHandler;

    /** LDAP configuration environment. */
    protected T config;

    /**
     * This will set the config parameters of this <code>Ldap</code>.
     *
     * @param  ldapConfig  <code>LdapConfig</code>
     */
    protected void setLdapConfig(final T ldapConfig) {
        if (this.config != null) {
            this.config.checkImmutable();
        }
        this.config = ldapConfig;
    }

    /**
     * This will perform an LDAP compare operation with the supplied filter and
     * dn. Note that to perform a <b>real</b> LDAP compare operation, your filter
     * must be of the form '(name=value)'. Any other filter expression will result
     * in a regular object level search operation. In either case the desired
     * result is achieved, but the underlying LDAP invocation is different.
     *
     * @param  dn  <code>String</code> name to compare
     * @param  filter  <code>String</code> expression to use for compare
     * @param  filterArgs  <code>Object[]</code> to substitute for variables in
     * the filter
     *
     * @return  <code>boolean</code> - result of compare operation
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected boolean compare(final String dn, final String filter, final Object[] filterArgs)
            throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Compare with the following parameters:");
            this.logger.debug("  dn = " + dn);
            this.logger.debug("  filter = " + filter);
            this.logger.debug("  filterArgs = " + Arrays.toString(filterArgs));
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        boolean success = false;
        LdapContext ctx = null;
        NamingEnumeration<SearchResult> en = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    en = ctx.search(dn, filter, filterArgs, LdapConfig.getCompareSearchControls());

                    if (en.hasMore()) {
                        success = true;
                    }

                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (en != null) {
                en.close();
            }
            if (ctx != null) {
                ctx.close();
            }
        }
        return success;
    }

    /**
     * This will query the LDAP with the supplied dn, filter, filter arguments,
     * and search controls. This method will perform a search whose scope is
     * defined in the search controls. The resulting <code>Iterator</code> is a
     * deep copy of the original search results. If filterArgs is null, then no
     * variable substitution will occur. See {@link
     * javax.naming.DirContext#search( String, String, Object[], SearchControls)}.
     *
     * @param  dn  <code>String</code> name to begin search at
     * @param  filter  <code>String</code> expression to use for the search
     * @param  filterArgs  <code>Object[]</code> to substitute for variables in
     * the filter
     * @param  searchControls  <code>SearchControls</code> to perform search with
     * @param  handler  <code>SearchResultHandler[]</code> to post process results
     *
     * @return  <code>Iterator</code> - of LDAP search results
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected Iterator<SearchResult> search(final String dn, final String filter, final Object[] filterArgs,
            final SearchControls searchControls, final SearchResultHandler... handler) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Search with the following parameters:");
            this.logger.debug("  dn = " + dn);
            this.logger.debug("  filter = " + filter);
            this.logger.debug("  filterArgs = " + Arrays.toString(filterArgs));
            this.logger.debug("  searchControls = " + searchControls);
            this.logger.debug("  handler = " + Arrays.toString(handler));
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        List<SearchResult> results = null;
        LdapContext ctx = null;
        NamingEnumeration<SearchResult> en = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    en = ctx.search(dn, filter, filterArgs, searchControls);

                    if (handler != null && handler.length > 0) {
                        final SearchCriteria sc = new SearchCriteria();
                        if (ctx != null && !"".equals(ctx.getNameInNamespace())) {
                            sc.setDn(ctx.getNameInNamespace());
                        } else {
                            sc.setDn(dn);
                        }
                        sc.setFilter(filter);
                        sc.setFilterArgs(filterArgs);
                        if (searchControls != null) {
                            sc.setReturnAttrs(searchControls.getReturningAttributes());
                        }
                        for (int j = 0; j < handler.length; j++) {
                            if (j == 0) {
                                results = handler[j].process(sc, en, this.config.getHandlerIgnoreExceptions());
                            } else {
                                results = handler[j].process(sc, results);
                            }
                        }
                    } else {
                        results = SR_COPY_RESULT_HANDLER.process(null, en,
                                this.config.getHandlerIgnoreExceptions());
                    }

                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (en != null) {
                en.close();
            }
            if (ctx != null) {
                ctx.close();
            }
        }
        return results.iterator();
    }

    /**
     * This will query the LDAP with the supplied dn, filter, filter arguments,
     * and search controls. See {@link #search(String, String, Object[],
     * SearchControls, SearchResultHandler...)}. The PagedResultsControl is used
     * in conjunction with {@link LdapConfig#getPagedResultsSize()} to produce the
     * results.
     *
     * @param  dn  <code>String</code> name to begin search at
     * @param  filter  <code>String</code> expression to use for the search
     * @param  filterArgs  <code>Object[]</code> to substitute for variables in
     * the filter
     * @param  searchControls  <code>SearchControls</code> to perform search with
     * @param  handler  <code>SearchResultHandler[]</code> to post process results
     *
     * @return  <code>Iterator</code> - of LDAP search results
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected Iterator<SearchResult> pagedSearch(final String dn, final String filter, final Object[] filterArgs,
            final SearchControls searchControls, final SearchResultHandler... handler) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Paginated search with the following parameters:");
            this.logger.debug("  dn = " + dn);
            this.logger.debug("  filter = " + filter);
            this.logger.debug("  filterArgs = " + Arrays.toString(filterArgs));
            this.logger.debug("  searchControls = " + searchControls);
            this.logger.debug("  handler = " + Arrays.toString(handler));
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        final List<SearchResult> results = new ArrayList<SearchResult>();
        LdapContext ctx = null;
        NamingEnumeration<SearchResult> en = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    byte[] cookie = null;
                    ctx = this.getContext();
                    ctx.setRequestControls(new Control[] {
                            new PagedResultsControl(this.config.getPagedResultsSize(), Control.CRITICAL), });
                    do {
                        List<SearchResult> pagedResults = null;
                        en = ctx.search(dn, filter, filterArgs, searchControls);

                        if (handler != null && handler.length > 0) {
                            final SearchCriteria sc = new SearchCriteria();
                            if (ctx != null && !"".equals(ctx.getNameInNamespace())) {
                                sc.setDn(ctx.getNameInNamespace());
                            } else {
                                sc.setDn(dn);
                            }
                            sc.setFilter(filter);
                            sc.setFilterArgs(filterArgs);
                            if (searchControls != null) {
                                sc.setReturnAttrs(searchControls.getReturningAttributes());
                            }
                            for (int j = 0; j < handler.length; j++) {
                                if (j == 0) {
                                    pagedResults = handler[j].process(sc, en,
                                            this.config.getHandlerIgnoreExceptions());
                                } else {
                                    pagedResults = handler[j].process(sc, pagedResults);
                                }
                            }
                        } else {
                            pagedResults = SR_COPY_RESULT_HANDLER.process(null, en,
                                    this.config.getHandlerIgnoreExceptions());
                        }

                        results.addAll(pagedResults);

                        final Control[] controls = ctx.getResponseControls();
                        if (controls != null) {
                            for (int j = 0; j < controls.length; j++) {
                                if (controls[j] instanceof PagedResultsResponseControl) {
                                    final PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[j];
                                    cookie = prrc.getCookie();
                                }
                            }
                        }

                        // re-activate paged results
                        ctx.setRequestControls(
                                new Control[] { new PagedResultsControl(this.config.getPagedResultsSize(), cookie,
                                        Control.CRITICAL), });

                    } while (cookie != null);

                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                } catch (IOException e) {
                    if (this.logger.isErrorEnabled()) {
                        this.logger.error("Could not encode page size into control", e);
                    }
                    throw new NamingException(e.getMessage());
                }
            }
        } finally {
            if (en != null) {
                en.close();
            }
            if (ctx != null) {
                ctx.close();
            }
        }
        return results.iterator();
    }

    /**
     * This will query the LDAP for the supplied dn, matching attributes and
     * return attributes. This method will always perform a one level search. The
     * resulting <code>Iterator</code> is a deep copy of the original search
     * results. If matchAttrs is empty or null then all objects in the target
     * context are returned. If retAttrs is null then all attributes will be
     * returned. If retAttrs is an empty array then no attributes will be
     * returned. See {@link javax.naming.DirContext#search(String, Attributes,
     * String[])}.
     *
     * @param  dn  <code>String</code> name to search in
     * @param  matchAttrs  <code>Attributes</code> attributes to match
     * @param  retAttrs  <code>String[]</code> attributes to return
     * @param  handler  <code>SearchResultHandler[]</code> to post process results
     *
     * @return  <code>Iterator</code> - of LDAP search results
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected Iterator<SearchResult> searchAttributes(final String dn, final Attributes matchAttrs,
            final String[] retAttrs, final SearchResultHandler... handler) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("One level search with the following parameters:");
            this.logger.debug("  dn = " + dn);
            this.logger.debug("  matchAttrs = " + matchAttrs);
            this.logger.debug("  retAttrs = " + (retAttrs == null ? "all attributes" : Arrays.toString(retAttrs)));
            this.logger.debug("  handler = " + Arrays.toString(handler));
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        List<SearchResult> results = null;
        LdapContext ctx = null;
        NamingEnumeration<SearchResult> en = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    en = ctx.search(dn, matchAttrs, retAttrs);

                    if (handler != null && handler.length > 0) {
                        final SearchCriteria sc = new SearchCriteria();
                        if (ctx != null && !"".equals(ctx.getNameInNamespace())) {
                            sc.setDn(ctx.getNameInNamespace());
                        } else {
                            sc.setDn(dn);
                        }
                        sc.setMatchAttrs(matchAttrs);
                        sc.setReturnAttrs(retAttrs);
                        if (handler != null && handler.length > 0) {
                            for (int j = 0; j < handler.length; j++) {
                                if (j == 0) {
                                    results = handler[j].process(sc, en, this.config.getHandlerIgnoreExceptions());
                                } else {
                                    results = handler[j].process(sc, results);
                                }
                            }
                        }
                    } else {
                        results = SR_COPY_RESULT_HANDLER.process(null, en,
                                this.config.getHandlerIgnoreExceptions());
                    }

                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (en != null) {
                en.close();
            }
            if (ctx != null) {
                ctx.close();
            }
        }
        return results.iterator();
    }

    /**
     * This will enumerate the names bounds to the specified context, along with
     * the class names of objects bound to them. The resulting <code>
     * Iterator</code> is a deep copy of the original search results. See {@link
     * javax.naming.Context#list(String)}.
     *
     * @param  dn  <code>String</code> LDAP context to list
     *
     * @return  <code>Iterator</code> - LDAP search result
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected Iterator<NameClassPair> list(final String dn) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("list with the following parameters:");
            this.logger.debug("  dn = " + dn);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        List<NameClassPair> results = null;
        LdapContext ctx = null;
        NamingEnumeration<NameClassPair> en = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    en = ctx.list(dn);

                    results = NCP_COPY_RESULT_HANDLER.process(null, en, this.config.getHandlerIgnoreExceptions());

                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (en != null) {
                en.close();
            }
            if (ctx != null) {
                ctx.close();
            }
        }
        return results.iterator();
    }

    /**
     * This will enumerate the names bounds to the specified context, along with
     * the objects bound to them. The resulting <code>Iterator</code> is a deep
     * copy of the original search results. See {@link
     * javax.naming.Context#listBindings(String)}.
     *
     * @param  dn  <code>String</code> LDAP context to list
     *
     * @return  <code>Iterator</code> - LDAP search result
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected Iterator<Binding> listBindings(final String dn) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("listBindings with the following parameters:");
            this.logger.debug("  dn = " + dn);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        List<Binding> results = null;
        LdapContext ctx = null;
        NamingEnumeration<Binding> en = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    en = ctx.listBindings(dn);

                    results = BINDING_COPY_RESULT_HANDLER.process(null, en,
                            this.config.getHandlerIgnoreExceptions());

                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (en != null) {
                en.close();
            }
            if (ctx != null) {
                ctx.close();
            }
        }
        return results.iterator();
    }

    /**
     * This will return the matching attributes associated with the supplied dn.
     * If retAttrs is null then all attributes will be returned. If retAttrs is an
     * empty array then no attributes will be returned. See {@link
     * javax.naming.DirContext#getAttributes(String, String[])}.
     *
     * @param  dn  <code>String</code> named object in the LDAP
     * @param  retAttrs  <code>String[]</code> attributes to return
     * @param  handler  <code>AttributeHandler[]</code> to post process results
     *
     * @return  <code>Attributes</code>
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected Attributes getAttributes(final String dn, final String[] retAttrs, final AttributeHandler... handler)
            throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Attribute search with the following parameters:");
            this.logger.debug("  dn = " + dn);
            this.logger.debug("  retAttrs = " + (retAttrs == null ? "all attributes" : Arrays.toString(retAttrs)));
            this.logger.debug("  handler = " + Arrays.toString(handler));
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        LdapContext ctx = null;
        Attributes attrs = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    attrs = ctx.getAttributes(dn, retAttrs);

                    if (handler != null && handler.length > 0) {
                        final SearchCriteria sc = new SearchCriteria();
                        if (ctx != null && !"".equals(ctx.getNameInNamespace())) {
                            sc.setDn(ctx.getNameInNamespace());
                        } else {
                            sc.setDn(dn);
                        }
                        for (int j = 0; j < handler.length; j++) {
                            attrs = AttributesProcessor.executeHandler(sc, attrs, handler[j],
                                    this.config.getHandlerIgnoreExceptions());
                        }
                    }

                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (ctx != null) {
                ctx.close();
            }
        }
        return attrs;
    }

    /**
     * This will return the LDAP schema associated with the supplied dn. The
     * resulting <code>Iterator</code> is a deep copy of the original search
     * results. See {@link javax.naming.DirContext#getSchema(String)}.
     *
     * @param  dn  <code>String</code> named object in the LDAP
     *
     * @return  <code>Iterator</code> - LDAP search result
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected Iterator<SearchResult> getSchema(final String dn) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Schema search with the following parameters:");
            this.logger.debug("  dn = " + dn);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        List<SearchResult> results = null;
        LdapContext ctx = null;
        DirContext schema = null;
        NamingEnumeration<SearchResult> en = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    schema = ctx.getSchema(dn);
                    en = schema.search("", null);

                    results = SR_COPY_RESULT_HANDLER.process(null, en, this.config.getHandlerIgnoreExceptions());

                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (schema != null) {
                schema.close();
            }
            if (en != null) {
                en.close();
            }
            if (ctx != null) {
                ctx.close();
            }
        }
        return results.iterator();
    }

    /**
     * This will modify the supplied attributes for the supplied value given by
     * the modification operation. modOp must be one of: ADD_ATTRIBUTE,
     * REPLACE_ATTRIBUTE, REMOVE_ATTRIBUTE. The order of the modifications is not
     * specified. Where possible, the modifications are performed atomically. See
     * {@link javax.naming.DirContext#modifyAttributes( String, int, Attributes)}.
     *
     * @param  dn  <code>String</code> named object in the LDAP
     * @param  modOp  <code>int</code> modification operation
     * @param  attrs  <code>Attributes</code> attributes to be used for the
     * operation, may be null
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected void modifyAttributes(final String dn, final int modOp, final Attributes attrs)
            throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Modify attributes with the following parameters:");
            this.logger.debug("  dn = " + dn);
            this.logger.debug("  modOp = " + modOp);
            this.logger.debug("  attrs = " + attrs);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        LdapContext ctx = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    ctx.modifyAttributes(dn, modOp, attrs);
                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (ctx != null) {
                ctx.close();
            }
        }
    }

    /**
     * This will modify the supplied dn using the supplied modifications. The
     * modifications are performed in the order specified. Each modification
     * specifies a modification operation code and an attribute on which to
     * operate. Where possible, the modifications are performed atomically. See
     * {@link javax.naming.DirContext#modifyAttributes(String,
     * ModificationItem[])}.
     *
     * @param  dn  <code>String</code> named object in the LDAP
     * @param  mods  <code>ModificationItem[]</code> modifications
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected void modifyAttributes(final String dn, final ModificationItem[] mods) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Modify attributes with the following parameters:");
            this.logger.debug("  dn = " + dn);
            this.logger.debug("  mods = " + Arrays.toString(mods));
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        LdapContext ctx = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    ctx.modifyAttributes(dn, mods);
                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (ctx != null) {
                ctx.close();
            }
        }
    }

    /**
     * This will create the supplied dn in the LDAP namespace with the supplied
     * attributes. See {@link javax.naming.DirContext#createSubcontext(String,
     * Attributes)}. Note that the context created by this operation is
     * immediately closed.
     *
     * @param  dn  <code>String</code> named object in the LDAP
     * @param  attrs  <code>Attributes</code> attributes to be added to this entry
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected void create(final String dn, final Attributes attrs) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Create name with the following parameters:");
            this.logger.debug("  dn = " + dn);
            this.logger.debug("  attrs = " + attrs);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        LdapContext ctx = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    ctx.createSubcontext(dn, attrs).close();
                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (ctx != null) {
                ctx.close();
            }
        }
    }

    /**
     * This will rename the supplied dn in the LDAP namespace. See {@link
     * javax.naming.Context#rename(String, String)}.
     *
     * @param  oldDn  <code>String</code> object to rename
     * @param  newDn  <code>String</code> new name
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected void rename(final String oldDn, final String newDn) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Rename name with the following parameters:");
            this.logger.debug("  oldDn = " + oldDn);
            this.logger.debug("  newDn = " + newDn);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        LdapContext ctx = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    ctx.rename(oldDn, newDn);
                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (ctx != null) {
                ctx.close();
            }
        }
    }

    /**
     * This will delete the supplied dn from the LDAP namespace. Note that this
     * method does not throw NameNotFoundException if the supplied dn does not
     * exist. See {@link javax.naming.Context#destroySubcontext(String)}.
     *
     * @param  dn  <code>String</code> named object in the LDAP
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected void delete(final String dn) throws NamingException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Delete name with the following parameters:");
            this.logger.debug("  dn = " + dn);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("  config = " + this.config.getEnvironment());
            }
        }

        LdapContext ctx = null;
        try {
            for (int i = 0; i <= this.config.getOperationRetry() || this.config.getOperationRetry() == -1; i++) {
                try {
                    ctx = this.getContext();
                    ctx.destroySubcontext(dn);
                    break;
                } catch (NamingException e) {
                    this.operationRetry(ctx, e, i);
                }
            }
        } finally {
            if (ctx != null) {
                ctx.close();
            }
        }
    }

    /**
     * This will establish a connection if one does not already exist by binding
     * to the LDAP using parameters given by {@link LdapConfig#getBindDn()} and
     * {@link LdapConfig#getBindCredential()}. If these parameters have not been
     * set then an anonymous bind will be attempted. This connection must be
     * closed using {@link #close}. Any method which requires an LDAP connection
     * will call this method independently. This method should only be used if you
     * need to verify that you can connect to the LDAP.
     *
     * @return  <code>boolean</code> - whether the connection was successful
     *
     * @throws  NamingException  if the LDAP cannot be reached
     */
    public synchronized boolean connect() throws NamingException {
        boolean success = false;
        if (this.connectionHandler == null) {
            this.connectionHandler = this.config.getConnectionHandler().newInstance();
        }
        if (this.connectionHandler.isConnected()) {
            success = true;
        } else {
            this.connectionHandler.connect(this.config.getBindDn(), this.config.getBindCredential());
            success = true;
        }
        return success;
    }

    /**
     * This will close the current connection to the LDAP and establish a new
     * connection to the LDAP using {@link #connect}.
     *
     * @return  <code>boolean</code> - whether the connection was successful
     *
     * @throws  NamingException  if the LDAP cannot be reached
     */
    public synchronized boolean reconnect() throws NamingException {
        this.close();
        return this.connect();
    }

    /** This will close the connection to the LDAP. */
    public synchronized void close() {
        if (this.connectionHandler != null) {
            try {
                this.connectionHandler.close();
            } catch (NamingException e) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Error closing connection with the LDAP", e);
                }
            } finally {
                this.connectionHandler = null;
            }
        }
    }

    /**
     * This will return an initialized connection to the LDAP.
     *
     * @return  <code>LdapContext</code>
     *
     * @throws  NamingException  if the LDAP returns an error
     */
    protected LdapContext getContext() throws NamingException {
        this.connect();
        if (this.connectionHandler != null && this.connectionHandler.isConnected()) {
            return this.connectionHandler.getLdapContext().newInstance(null);
        } else {
            return null;
        }
    }

    /**
     * Confirms whether the supplied exception matches an exception from {@link
     * LdapConfig#getOperationRetryExceptions()} and the supplied count is less
     * than {@link LdapConfig#getOperationRetry()}. {@link
     * LdapConfig#getOperationRetryWait()} is used in conjunction with {@link
     * LdapConfig#getOperationRetryBackoff()} to delay retries. Calls {@link
     * #close()} if no exception is thrown, which allows the client to reconnect
     * when the operation is performed again.
     *
     * @param  ctx  <code>LdapContext</code> that performed the operation
     * @param  e  <code>NamingException</code> that was thrown
     * @param  count  <code>int</code> operation attempts
     *
     * @throws  NamingException  if the operation won't be retried
     */
    protected void operationRetry(final LdapContext ctx, final NamingException e, final int count)
            throws NamingException {
        boolean ignoreException = false;
        final Class<?>[] ignore = this.config.getOperationRetryExceptions();
        if (ignore != null && ignore.length > 0) {
            for (Class<?> ne : ignore) {
                if (ne.isInstance(e)) {
                    ignoreException = true;
                    break;
                }
            }
        }
        if (ignoreException && (count < this.config.getOperationRetry() || this.config.getOperationRetry() == -1)) {
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("Error performing LDAP operation, " + "retrying (attempt " + count + ")", e);
            }
            if (ctx != null) {
                ctx.close();
            }
            this.close();
            if (this.config.getOperationRetryWait() > 0) {
                long sleepTime = this.config.getOperationRetryWait();
                if (this.config.getOperationRetryBackoff() > 0 && count > 0) {
                    sleepTime = sleepTime * this.config.getOperationRetryBackoff() * count;
                }
                try {
                    Thread.sleep(sleepTime);
                } catch (InterruptedException ie) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Operation retry wait interrupted", e);
                    }
                }
            }
        } else {
            throw e;
        }
    }

    /**
     * Provides a descriptive string representation of this instance.
     *
     * @return  String of the form $Classname@hashCode::config=$config.
     */
    @Override
    public String toString() {
        return String.format("%s@%d::config=%s", this.getClass().getName(), this.hashCode(), this.config);
    }

    /**
     * Called by the garbage collector on an object when garbage collection
     * determines that there are no more references to the object.
     *
     * @throws  Throwable  if an exception is thrown by this method
     */
    protected void finalize() throws Throwable {
        try {
            this.close();
        } finally {
            super.finalize();
        }
    }
}