[libvt-ldap-java] 01/02: Imported Upstream version 3.3.7

Matthew Vernon matthew at moszumanska.debian.org
Tue Jun 17 15:58:06 UTC 2014


This is an automated email from the git hooks/post-receive script.

matthew pushed a commit to annotated tag debian/3.3.7-2
in repository libvt-ldap-java.

commit 34f68ccea91eda4093cebe6340a5d34aa1c484b0
Author: Matthew Vernon <mcv21 at cam.ac.uk>
Date:   Tue Jun 17 16:45:23 2014 +0100

    Imported Upstream version 3.3.7
---
 .project                                           |   23 +
 LICENSE.apache2.txt                                |  203 +++
 LICENSE.lgpl.txt                                   |  165 ++
 README.txt                                         |   10 +
 bin/ldapauth                                       |   28 +
 bin/ldapauth.bat                                   |   24 +
 bin/ldapjaas                                       |   31 +
 bin/ldapjaas.bat                                   |   26 +
 bin/ldapsearch                                     |   31 +
 bin/ldapsearch.bat                                 |   24 +
 licenses/commons-cli.license                       |  202 ++
 licenses/commons-codec.license                     |  202 ++
 licenses/commons-logging.license                   |  202 ++
 licenses/dom4j.license                             |   40 +
 pom.xml                                            |  386 ++++
 properties/krb5_jaas.config                        |   11 +
 properties/ldap.properties                         |  122 ++
 properties/ldap_jaas.config                        |    8 +
 src/main/assembly/vt-middleware.xml                |   74 +
 src/main/checkstyle/checkstyle_checks.xml          |  199 ++
 src/main/checkstyle/checkstyle_header              |   13 +
 src/main/checkstyle/suppressions.xml               |   15 +
 .../java/edu/vt/middleware/ldap/AbstractCli.java   |  278 +++
 .../java/edu/vt/middleware/ldap/AbstractLdap.java  | 1166 ++++++++++++
 .../edu/vt/middleware/ldap/AttributesFactory.java  |  192 ++
 src/main/java/edu/vt/middleware/ldap/BaseLdap.java |   52 +
 src/main/java/edu/vt/middleware/ldap/Ldap.java     |  747 ++++++++
 src/main/java/edu/vt/middleware/ldap/LdapCli.java  |  179 ++
 .../java/edu/vt/middleware/ldap/LdapConfig.java    | 1921 ++++++++++++++++++++
 .../java/edu/vt/middleware/ldap/LdapConstants.java |  352 ++++
 .../java/edu/vt/middleware/ldap/LdapSearch.java    |  172 ++
 src/main/java/edu/vt/middleware/ldap/LdapUtil.java |  222 +++
 .../java/edu/vt/middleware/ldap/SearchFilter.java  |  147 ++
 .../ldap/auth/AbstractAuthenticator.java           |  242 +++
 .../edu/vt/middleware/ldap/auth/Authenticator.java |  366 ++++
 .../vt/middleware/ldap/auth/AuthenticatorCli.java  |  191 ++
 .../middleware/ldap/auth/AuthenticatorConfig.java  |  555 ++++++
 .../ldap/auth/AuthorizationException.java          |   49 +
 .../middleware/ldap/auth/ConstructDnResolver.java  |  114 ++
 .../edu/vt/middleware/ldap/auth/DnResolver.java    |   58 +
 .../vt/middleware/ldap/auth/NoopDnResolver.java    |   73 +
 .../vt/middleware/ldap/auth/SearchDnResolver.java  |  185 ++
 .../handler/AbstractAuthenticationHandler.java     |   56 +
 .../ldap/auth/handler/AuthenticationCriteria.java  |  102 ++
 .../ldap/auth/handler/AuthenticationHandler.java   |   62 +
 .../auth/handler/AuthenticationResultHandler.java  |   35 +
 .../ldap/auth/handler/AuthorizationHandler.java    |   46 +
 .../auth/handler/BindAuthenticationHandler.java    |   62 +
 .../auth/handler/CompareAuthenticationHandler.java |  128 ++
 .../auth/handler/CompareAuthorizationHandler.java  |  124 ++
 .../ldap/bean/AbstractLdapAttribute.java           |  160 ++
 .../ldap/bean/AbstractLdapAttributes.java          |  216 +++
 .../vt/middleware/ldap/bean/AbstractLdapBean.java  |   73 +
 .../vt/middleware/ldap/bean/AbstractLdapEntry.java |  123 ++
 .../middleware/ldap/bean/AbstractLdapResult.java   |  171 ++
 .../edu/vt/middleware/ldap/bean/LdapAttribute.java |   84 +
 .../vt/middleware/ldap/bean/LdapAttributes.java    |  166 ++
 .../vt/middleware/ldap/bean/LdapBeanFactory.java   |   57 +
 .../vt/middleware/ldap/bean/LdapBeanProvider.java  |  107 ++
 .../edu/vt/middleware/ldap/bean/LdapEntry.java     |   79 +
 .../edu/vt/middleware/ldap/bean/LdapResult.java    |  124 ++
 .../ldap/bean/OrderedLdapBeanFactory.java          |  135 ++
 .../ldap/bean/SortedLdapBeanFactory.java           |  137 ++
 .../ldap/bean/UnorderedLdapBeanFactory.java        |  135 ++
 .../edu/vt/middleware/ldap/dsml/AbstractDsml.java  |  387 ++++
 .../middleware/ldap/dsml/DsmlResultConverter.java  |  174 ++
 .../edu/vt/middleware/ldap/dsml/DsmlSearch.java    |  112 ++
 .../java/edu/vt/middleware/ldap/dsml/Dsmlv1.java   |  247 +++
 .../java/edu/vt/middleware/ldap/dsml/Dsmlv2.java   |  148 ++
 .../ldap/handler/AbstractConnectionHandler.java    |  331 ++++
 .../ldap/handler/AbstractResultHandler.java        |  150 ++
 .../middleware/ldap/handler/AttributeHandler.java  |   24 +
 .../ldap/handler/AttributesProcessor.java          |   89 +
 .../ldap/handler/BinaryAttributeHandler.java       |   45 +
 .../ldap/handler/BinarySearchResultHandler.java    |   33 +
 .../ldap/handler/CaseChangeAttributeHandler.java   |  113 ++
 .../handler/CaseChangeSearchResultHandler.java     |  150 ++
 .../middleware/ldap/handler/ConnectionHandler.java |  143 ++
 .../ldap/handler/CopyAttributeHandler.java         |   73 +
 .../middleware/ldap/handler/CopyResultHandler.java |   46 +
 .../ldap/handler/CopySearchResultHandler.java      |  111 ++
 .../ldap/handler/DefaultConnectionHandler.java     |  153 ++
 .../ldap/handler/EntryDnSearchResultHandler.java   |  101 +
 .../ldap/handler/ExtendedAttributeHandler.java     |   45 +
 .../ldap/handler/ExtendedSearchResultHandler.java  |   45 +
 .../ldap/handler/FqdnSearchResultHandler.java      |  126 ++
 .../ldap/handler/MergeSearchResultHandler.java     |  137 ++
 .../ldap/handler/RecursiveAttributeHandler.java    |  187 ++
 .../ldap/handler/RecursiveSearchResultHandler.java |  323 ++++
 .../vt/middleware/ldap/handler/ResultHandler.java  |   77 +
 .../vt/middleware/ldap/handler/SearchCriteria.java |  186 ++
 .../ldap/handler/SearchResultHandler.java          |   43 +
 .../ldap/handler/TlsConnectionHandler.java         |  266 +++
 .../middleware/ldap/jaas/AbstractLoginModule.java  |  505 +++++
 .../vt/middleware/ldap/jaas/JaasAuthenticator.java |   93 +
 .../vt/middleware/ldap/jaas/LdapCredential.java    |   93 +
 .../ldap/jaas/LdapDnAuthorizationModule.java       |  157 ++
 .../vt/middleware/ldap/jaas/LdapDnPrincipal.java   |  138 ++
 .../edu/vt/middleware/ldap/jaas/LdapGroup.java     |  120 ++
 .../vt/middleware/ldap/jaas/LdapLoginModule.java   |  205 +++
 .../edu/vt/middleware/ldap/jaas/LdapPrincipal.java |  138 ++
 .../java/edu/vt/middleware/ldap/jaas/LdapRole.java |  119 ++
 .../ldap/jaas/LdapRoleAuthorizationModule.java     |  191 ++
 .../java/edu/vt/middleware/ldap/ldif/Ldif.java     |  398 ++++
 .../middleware/ldap/ldif/LdifResultConverter.java  |  116 ++
 .../edu/vt/middleware/ldap/ldif/LdifSearch.java    |   69 +
 .../middleware/ldap/pool/AbstractLdapFactory.java  |  175 ++
 .../vt/middleware/ldap/pool/AbstractLdapPool.java  |  664 +++++++
 .../vt/middleware/ldap/pool/BlockingLdapPool.java  |  323 ++++
 .../ldap/pool/BlockingTimeoutException.java        |   65 +
 .../middleware/ldap/pool/CloseLdapPassivator.java  |   44 +
 .../middleware/ldap/pool/CompareLdapValidator.java |  121 ++
 .../middleware/ldap/pool/ConnectLdapActivator.java |   51 +
 .../middleware/ldap/pool/ConnectLdapValidator.java |   50 +
 .../middleware/ldap/pool/DefaultLdapFactory.java   |  126 ++
 .../ldap/pool/LdapActivationException.java         |   65 +
 .../edu/vt/middleware/ldap/pool/LdapActivator.java |   39 +
 .../edu/vt/middleware/ldap/pool/LdapFactory.java   |   75 +
 .../vt/middleware/ldap/pool/LdapPassivator.java    |   39 +
 .../java/edu/vt/middleware/ldap/pool/LdapPool.java |  107 ++
 .../vt/middleware/ldap/pool/LdapPoolConfig.java    |  379 ++++
 .../vt/middleware/ldap/pool/LdapPoolException.java |   65 +
 .../ldap/pool/LdapPoolExhaustedException.java      |   65 +
 .../ldap/pool/LdapValidationException.java         |   65 +
 .../edu/vt/middleware/ldap/pool/LdapValidator.java |   39 +
 .../ldap/pool/PoolInterruptedException.java        |   65 +
 .../edu/vt/middleware/ldap/pool/PrunePoolTask.java |   73 +
 .../vt/middleware/ldap/pool/SharedLdapPool.java    |  224 +++
 .../vt/middleware/ldap/pool/SoftLimitLdapPool.java |  135 ++
 .../vt/middleware/ldap/pool/ValidatePoolTask.java  |   71 +
 .../ldap/props/AbstractPropertyConfig.java         |  143 ++
 .../ldap/props/AbstractPropertyInvoker.java        |  264 +++
 .../edu/vt/middleware/ldap/props/ConfigParser.java |  143 ++
 .../ldap/props/LdapConfigPropertyInvoker.java      |  239 +++
 .../vt/middleware/ldap/props/LdapProperties.java   |  188 ++
 .../vt/middleware/ldap/props/PropertyConfig.java   |   72 +
 .../ldap/props/SimplePropertyInvoker.java          |   88 +
 .../middleware/ldap/servlets/AttributeServlet.java |  240 +++
 .../vt/middleware/ldap/servlets/CommonServlet.java |  108 ++
 .../vt/middleware/ldap/servlets/LoginServlet.java  |  215 +++
 .../vt/middleware/ldap/servlets/LogoutServlet.java |   92 +
 .../vt/middleware/ldap/servlets/SearchServlet.java |  254 +++
 .../middleware/ldap/servlets/ServletConstants.java |  108 ++
 .../servlets/session/DefaultSessionManager.java    |   97 +
 .../ldap/servlets/session/SessionManager.java      |   92 +
 .../ldap/ssl/AbstractCredentialReader.java         |  111 ++
 .../ldap/ssl/AbstractSSLContextInitializer.java    |   55 +
 .../ldap/ssl/AbstractTLSSocketFactory.java         |  371 ++++
 .../middleware/ldap/ssl/AggregateTrustManager.java |   99 +
 .../ldap/ssl/CertificateHostnameVerifier.java      |   37 +
 .../vt/middleware/ldap/ssl/CredentialConfig.java   |   43 +
 .../ldap/ssl/CredentialConfigParser.java           |  205 +++
 .../vt/middleware/ldap/ssl/CredentialReader.java   |   62 +
 .../ldap/ssl/DefaultHostnameVerifier.java          |  355 ++++
 .../ldap/ssl/DefaultSSLContextInitializer.java     |   74 +
 .../ldap/ssl/HostnameVerifyingTrustManager.java    |   99 +
 .../ldap/ssl/KeyStoreCredentialConfig.java         |  213 +++
 .../ldap/ssl/KeyStoreCredentialReader.java         |   74 +
 .../ldap/ssl/KeyStoreSSLContextInitializer.java    |  106 ++
 .../ldap/ssl/PrivateKeyCredentialReader.java       |   61 +
 .../middleware/ldap/ssl/SSLContextInitializer.java |   66 +
 .../ldap/ssl/SingletonTLSSocketFactory.java        |   75 +
 .../vt/middleware/ldap/ssl/TLSSocketFactory.java   |  116 ++
 .../ldap/ssl/ThreadLocalTLSSocketFactory.java      |  143 ++
 .../ldap/ssl/X509CertificateCredentialReader.java  |   41 +
 .../ldap/ssl/X509CertificatesCredentialReader.java |   51 +
 .../middleware/ldap/ssl/X509CredentialConfig.java  |  140 ++
 .../ldap/ssl/X509SSLContextInitializer.java        |  162 ++
 .../resources/edu/vt/middleware/ldap/LdapCli.args  |   24 +
 .../edu/vt/middleware/ldap/LdapCli.examples        |   19 +
 .../vt/middleware/ldap/auth/AuthenticatorCli.args  |   32 +
 .../middleware/ldap/auth/AuthenticatorCli.examples |   18 +
 .../vt/middleware/ldap/AnyHostnameVerifier.java    |   72 +
 .../java/edu/vt/middleware/ldap/LdapCliTest.java   |  104 ++
 .../edu/vt/middleware/ldap/LdapConfigTest.java     |   92 +
 .../vt/middleware/ldap/LdapConnStrategyTest.java   |   70 +
 .../java/edu/vt/middleware/ldap/LdapConnTest.java  |   58 +
 src/test/java/edu/vt/middleware/ldap/LdapTest.java | 1529 ++++++++++++++++
 .../java/edu/vt/middleware/ldap/RetryLdap.java     |  100 +
 .../java/edu/vt/middleware/ldap/SpringTest.java    |   58 +
 src/test/java/edu/vt/middleware/ldap/TestUtil.java |  409 +++++
 .../java/edu/vt/middleware/ldap/WikiCodeTest.java  |  490 +++++
 .../middleware/ldap/auth/AuthenticatorCliTest.java |  117 ++
 .../ldap/auth/AuthenticatorLoadTest.java           |  297 +++
 .../vt/middleware/ldap/auth/AuthenticatorTest.java |  847 +++++++++
 .../handler/TestAuthenticationResultHandler.java   |   49 +
 .../auth/handler/TestAuthorizationHandler.java     |   69 +
 .../vt/middleware/ldap/bean/LdapResultTest.java    |  111 ++
 .../java/edu/vt/middleware/ldap/dsml/DsmlTest.java |  207 +++
 .../middleware/ldap/jaas/LdapLoginModuleTest.java  |  771 ++++++++
 .../middleware/ldap/jaas/TestCallbackHandler.java  |   77 +
 .../vt/middleware/ldap/jaas/TestLoginModule.java   |  112 ++
 .../java/edu/vt/middleware/ldap/ldif/LdifTest.java |  176 ++
 .../edu/vt/middleware/ldap/pool/LdapPoolTest.java  | 1109 +++++++++++
 .../ldap/pool/commons/CommonsLdapPool.java         |   46 +
 .../commons/DefaultLdapPoolableObjectFactory.java  |   64 +
 .../ldap/servlets/AttributeServletTest.java        |  161 ++
 .../ldap/servlets/SearchServletTest.java           |  211 +++
 .../vt/middleware/ldap/servlets/SessionCheck.java  |   60 +
 .../ldap/servlets/session/SessionManagerTest.java  |  144 ++
 .../ldap/ssl/DefaultHostnameVerifierTest.java      |  344 ++++
 .../ldap/ssl/SunTLSHostnameVerifier.java           |   69 +
 .../middleware/ldap/ssl/TLSSocketFactoryTest.java  |  208 +++
 src/test/resources/ed.keystore                     |  Bin 0 -> 5112 bytes
 src/test/resources/ed.trust.crt                    |   42 +
 src/test/resources/ed.truststore                   |  Bin 0 -> 1905 bytes
 .../edu/vt/middleware/ldap/binaryResults.ldif      |    5 +
 .../edu/vt/middleware/ldap/createGroupEntry-2.ldif |    6 +
 .../edu/vt/middleware/ldap/createGroupEntry-3.ldif |    6 +
 .../edu/vt/middleware/ldap/createGroupEntry-4.ldif |    6 +
 .../edu/vt/middleware/ldap/createGroupEntry-5.ldif |    6 +
 .../edu/vt/middleware/ldap/createGroupEntry-6.ldif |    6 +
 .../edu/vt/middleware/ldap/createGroupEntry-7.ldif |    6 +
 .../edu/vt/middleware/ldap/createGroupEntry-8.ldif |    6 +
 .../edu/vt/middleware/ldap/createGroupEntry-9.ldif |    6 +
 .../edu/vt/middleware/ldap/createLdapEntry-10.ldif |   16 +
 .../edu/vt/middleware/ldap/createLdapEntry-11.ldif |   15 +
 .../edu/vt/middleware/ldap/createLdapEntry-12.ldif |   15 +
 .../edu/vt/middleware/ldap/createLdapEntry-2.ldif  |   16 +
 .../edu/vt/middleware/ldap/createLdapEntry-3.ldif  |   16 +
 .../edu/vt/middleware/ldap/createLdapEntry-4.ldif  |   16 +
 .../edu/vt/middleware/ldap/createLdapEntry-5.ldif  |   16 +
 .../edu/vt/middleware/ldap/createLdapEntry-6.ldif  |   16 +
 .../edu/vt/middleware/ldap/createLdapEntry-7.ldif  |   24 +
 .../edu/vt/middleware/ldap/createLdapEntry-8.ldif  |   16 +
 .../edu/vt/middleware/ldap/createLdapEntry-9.ldif  |   16 +
 .../edu/vt/middleware/ldap/dfisher.dsmlv1          |   74 +
 .../edu/vt/middleware/ldap/dfisher.dsmlv2          |   77 +
 .../resources/edu/vt/middleware/ldap/dfisher.ldif  |   28 +
 .../edu/vt/middleware/ldap/dfisher.sorted.dsmlv1   |   74 +
 .../edu/vt/middleware/ldap/dfisher.sorted.dsmlv2   |   77 +
 .../edu/vt/middleware/ldap/dfisher.sorted.ldif     |   28 +
 .../edu/vt/middleware/ldap/getSchemaResults.ldif   |   12 +
 .../resources/edu/vt/middleware/ldap/image.jpg     |  Bin 0 -> 371 bytes
 .../vt/middleware/ldap/mergeDuplicateResults.ldif  |   19 +
 .../edu/vt/middleware/ldap/mergeResults.ldif       |   13 +
 .../edu/vt/middleware/ldap/multipleEntriesIn.ldif  |  102 ++
 .../edu/vt/middleware/ldap/multipleEntriesOut.ldif |   76 +
 .../edu/vt/middleware/ldap/pagedResults.ldif       |   32 +
 .../ldap/recursiveAttributeHandlerResults.ldif     |    9 +
 .../ldap/recursiveSearchResultHandlerResults.ldif  |   13 +
 .../middleware/ldap/searchAttributesResults-2.ldif |    5 +
 .../edu/vt/middleware/ldap/searchResults-10.ldif   |    5 +
 .../edu/vt/middleware/ldap/searchResults-12.ldif   |   15 +
 .../edu/vt/middleware/ldap/searchResults-2.ldif    |    5 +
 .../edu/vt/middleware/ldap/searchResults-3.ldif    |    5 +
 .../edu/vt/middleware/ldap/searchResults-4.ldif    |    5 +
 .../edu/vt/middleware/ldap/searchResults-5.ldif    |    5 +
 .../edu/vt/middleware/ldap/searchResults-6.ldif    |    5 +
 .../edu/vt/middleware/ldap/searchResults-7.ldif    |    5 +
 .../edu/vt/middleware/ldap/searchResults-8.ldif    |    6 +
 .../edu/vt/middleware/ldap/searchResults-9.ldif    |    5 +
 .../edu/vt/middleware/ldap/specialChars-2.ldif     |   10 +
 .../edu/vt/middleware/ldap/specialChars.ldif       |    9 +
 src/test/resources/krb5.keytab                     |  Bin 0 -> 102 bytes
 src/test/resources/ldap.conn.properties            |   30 +
 src/test/resources/ldap.cram-md5.properties        |   18 +
 src/test/resources/ldap.digest-md5.properties      |   18 +
 src/test/resources/ldap.gssapi.properties          |   19 +
 src/test/resources/ldap.null.properties            |   52 +
 src/test/resources/ldap.parser.properties          |   50 +
 src/test/resources/ldap.pool.properties            |   10 +
 src/test/resources/ldap.properties                 |   47 +
 src/test/resources/ldap.sasl.properties            |   31 +
 src/test/resources/ldap.setup.properties           |   17 +
 src/test/resources/ldap.ssl.properties             |   27 +
 src/test/resources/ldap.tls.load.properties        |   30 +
 src/test/resources/ldap.tls.properties             |   43 +
 src/test/resources/ldap_jaas.config                |  279 +++
 src/test/resources/log4j.xml                       |   32 +
 src/test/resources/spring-context.xml              |   42 +
 src/test/resources/spring-pool-context.xml         |   60 +
 src/test/resources/vt-ldap.truststore              |  Bin 0 -> 3484 bytes
 src/test/resources/web.xml                         |  115 ++
 src/test/testng/testng.xml                         |  355 ++++
 275 files changed, 36911 insertions(+)

diff --git a/.project b/.project
new file mode 100644
index 0000000..a8b8d1d
--- /dev/null
+++ b/.project
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>vt-ldap</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.maven.ide.eclipse.maven2Builder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.maven.ide.eclipse.maven2Nature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/LICENSE.apache2.txt b/LICENSE.apache2.txt
new file mode 100644
index 0000000..6b0b127
--- /dev/null
+++ b/LICENSE.apache2.txt
@@ -0,0 +1,203 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
diff --git a/LICENSE.lgpl.txt b/LICENSE.lgpl.txt
new file mode 100644
index 0000000..cca7fc2
--- /dev/null
+++ b/LICENSE.lgpl.txt
@@ -0,0 +1,165 @@
+		   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..534c3ab
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,10 @@
+LDAP ${project.version} README
+
+    This is the ${project.version} release of the VT LDAP Java libraries.
+    It is dual licensed under both the LGPL and Apache 2.
+    If you have questions or comments about this library send e-mail to
+    vt-middleware-users at googlegroups.com.
+
+DOCUMENTATION
+    See the wiki: http://code.google.com/p/vt-middleware/wiki/vtldap
+
diff --git a/bin/ldapauth b/bin/ldapauth
new file mode 100755
index 0000000..16ac216
--- /dev/null
+++ b/bin/ldapauth
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+JAVA=java
+
+#KEYSTORE_OPTS="-Djavax.net.ssl.keyStore= -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.ssl.keyStoreType=BKS"
+
+#TRUSTSTORE_OPTS="-Djavax.net.ssl.trustStore= -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=BKS"
+
+# uncomment for debug logging
+#LOGGING_OPTS="-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog -Dorg.apache.commons.logging.simplelog.log.edu.vt.middleware.ldap=debug"
+
+JAVA_OPTS="${JAVA_OPTS} ${KEYSTORE_OPTS} ${TRUSTSTORE_OPTS} ${LOGGING_OPTS}"
+
+if [ "x$VTLDAP_HOME" = "x" ]; then
+  PREFIX=`dirname $0`/..
+else
+  PREFIX="$VTLDAP_HOME"
+fi
+
+CLASSPATH="${PREFIX}/jars/vt-ldap-${project.version}.jar"
+CLASSPATH=${CLASSPATH}:${PREFIX}/properties
+for JAR in `ls ${PREFIX}/lib/*.jar` ; do
+  CLASSPATH=${CLASSPATH}:$JAR
+done
+
+${JAVA} ${JAVA_OPTS} -cp ${CLASSPATH} \
+  edu.vt.middleware.ldap.auth.AuthenticatorCli $@
+
diff --git a/bin/ldapauth.bat b/bin/ldapauth.bat
new file mode 100644
index 0000000..67c141e
--- /dev/null
+++ b/bin/ldapauth.bat
@@ -0,0 +1,24 @@
+ at echo off
+if "%OS%" == "Windows_NT" setlocal
+
+if not defined JAVA_HOME goto no_java_home
+if not defined VTLDAP_HOME goto no_vtldap_home
+
+set JAVA=%JAVA_HOME%\bin\java
+
+set LDAP_JAR=%VTLDAP_HOME%\jars\vt-ldap-${project.version}.jar
+set LIBDIR=%VTLDAP_HOME%\lib
+
+set CLASSPATH=%LIBDIR%\commons-cli-1.2.jar;%LIBDIR%\commons-codec-1.4.jar;%LIBDIR%\commons-logging-1.1.3.jar;%LIBDIR%\dom4j-1.6.1.jar;%LDAP_JAR%
+
+call "%JAVA%" -cp "%CLASSPATH%" edu.vt.middleware.ldap.auth.AuthenticatorCli %*
+goto end
+
+:no_vtldap_home
+echo ERROR: VTLDAP_HOME environment variable must be set to VT Ldap install path.
+goto end
+
+:no_java_home
+echo ERROR: JAVA_HOME environment variable must be set to JRE/JDK install path.
+
+:end
diff --git a/bin/ldapjaas b/bin/ldapjaas
new file mode 100755
index 0000000..62da670
--- /dev/null
+++ b/bin/ldapjaas
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+JAVA=java
+
+# sample options for jaas
+JAAS_OPTS="-Djava.security.auth.login.config=properties/ldap_jaas.config"
+
+#KEYSTORE_OPTS="-Djavax.net.ssl.keyStore= -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.ssl.keyStoreType=BKS"
+
+#TRUSTSTORE_OPTS="-Djavax.net.ssl.trustStore= -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=BKS"
+
+# uncomment for debug logging
+#LOGGING_OPTS="-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog -Dorg.apache.commons.logging.simplelog.log.edu.vt.middleware.ldap=debug"
+
+JAVA_OPTS="${JAVA_OPTS} ${JAAS_OPTS} ${KEYSTORE_OPTS} ${TRUSTSTORE_OPTS} ${LOGGING_OPTS}"
+
+if [ "x$VTLDAP_HOME" = "x" ]; then
+  PREFIX=`dirname $0`/..
+else
+  PREFIX="$VTLDAP_HOME"
+fi
+
+CLASSPATH="${PREFIX}/jars/vt-ldap-${project.version}.jar"
+CLASSPATH=${CLASSPATH}:${PREFIX}/properties
+for JAR in `ls ${PREFIX}/lib/*.jar` ; do
+  CLASSPATH=${CLASSPATH}:$JAR
+done
+
+${JAVA} ${JAVA_OPTS} -cp ${CLASSPATH} \
+  edu.vt.middleware.ldap.jaas.LdapLoginModule $@
+
diff --git a/bin/ldapjaas.bat b/bin/ldapjaas.bat
new file mode 100644
index 0000000..0c9d0e1
--- /dev/null
+++ b/bin/ldapjaas.bat
@@ -0,0 +1,26 @@
+ at echo off
+if "%OS%" == "Windows_NT" setlocal
+
+if not defined JAVA_HOME goto no_java_home
+if not defined VTLDAP_HOME goto no_vtldap_home
+
+set JAVA=%JAVA_HOME%\bin\java
+
+set LDAP_JAR=%VTLDAP_HOME%\jars\vt-ldap-${project.version}.jar
+set LIBDIR=%VTLDAP_HOME%\lib
+
+set JAAS_OPTS=-Djava.security.auth.login.config=%VTLDAP_HOME%\properties\ldap_jaas.config
+
+set CLASSPATH=%LIBDIR%\commons-cli-1.2.jar;%LIBDIR%\commons-codec-1.4.jar;%LIBDIR%\commons-logging-1.1.3.jar;%LIBDIR%\dom4j-1.6.1.jar;%LDAP_JAR%
+
+call "%JAVA%" "%JAAS_OPTS%" -cp "%CLASSPATH%" edu.vt.middleware.ldap.jaas.LdapLoginModule %*
+goto end
+
+:no_vtldap_home
+echo ERROR: VTLDAP_HOME environment variable must be set to VT Ldap install path.
+goto end
+
+:no_java_home
+echo ERROR: JAVA_HOME environment variable must be set to JRE/JDK install path.
+
+:end
diff --git a/bin/ldapsearch b/bin/ldapsearch
new file mode 100755
index 0000000..710f127
--- /dev/null
+++ b/bin/ldapsearch
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+JAVA=java
+
+# sample options for kerberos
+#KRB5_OPTS="-Djava.security.auth.login.config=properties/krb5_jaas.config -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.krb5.realm=VT.EDU -Djava.security.krb5.kdc=directory.vt.edu"
+
+#KEYSTORE_OPTS="-Djavax.net.ssl.keyStore= -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.ssl.keyStoreType=BKS"
+
+#TRUSTSTORE_OPTS="-Djavax.net.ssl.trustStore= -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=BKS"
+
+# uncomment for debug logging
+#LOGGING_OPTS="-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog -Dorg.apache.commons.logging.simplelog.log.edu.vt.middleware.ldap=debug"
+
+JAVA_OPTS="${JAVA_OPTS} ${KRB5_OPTS} ${KEYSTORE_OPTS} ${TRUSTSTORE_OPTS} ${LOGGING_OPTS}"
+
+if [ "x$VTLDAP_HOME" = "x" ]; then
+  PREFIX=`dirname $0`/..
+else
+  PREFIX="$VTLDAP_HOME"
+fi
+
+CLASSPATH="${PREFIX}/jars/vt-ldap-${project.version}.jar"
+CLASSPATH=${CLASSPATH}:${PREFIX}/properties
+for JAR in `ls ${PREFIX}/lib/*.jar` ; do
+  CLASSPATH=${CLASSPATH}:$JAR
+done
+
+${JAVA} ${JAVA_OPTS} -cp ${CLASSPATH} \
+  edu.vt.middleware.ldap.LdapCli $@
+
diff --git a/bin/ldapsearch.bat b/bin/ldapsearch.bat
new file mode 100755
index 0000000..2e58dac
--- /dev/null
+++ b/bin/ldapsearch.bat
@@ -0,0 +1,24 @@
+ at echo off
+if "%OS%" == "Windows_NT" setlocal
+
+if not defined JAVA_HOME goto no_java_home
+if not defined VTLDAP_HOME goto no_vtldap_home
+
+set JAVA=%JAVA_HOME%\bin\java
+
+set LDAP_JAR=%VTLDAP_HOME%\jars\vt-ldap-${project.version}.jar
+set LIBDIR=%VTLDAP_HOME%\lib
+
+set CLASSPATH=%LIBDIR%\commons-cli-1.2.jar;%LIBDIR%\commons-codec-1.4.jar;%LIBDIR%\commons-logging-1.1.3.jar;%LIBDIR%\dom4j-1.6.1.jar;%LDAP_JAR%
+
+call "%JAVA%" -cp "%CLASSPATH%" edu.vt.middleware.ldap.LdapCli %*
+goto end
+
+:no_vtldap_home
+echo ERROR: VTLDAP_HOME environment variable must be set to VT Ldap install path.
+goto end
+
+:no_java_home
+echo ERROR: JAVA_HOME environment variable must be set to JRE/JDK install path.
+
+:end
diff --git a/licenses/commons-cli.license b/licenses/commons-cli.license
new file mode 100644
index 0000000..75b5248
--- /dev/null
+++ b/licenses/commons-cli.license
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/licenses/commons-codec.license b/licenses/commons-codec.license
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/licenses/commons-codec.license
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/licenses/commons-logging.license b/licenses/commons-logging.license
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/licenses/commons-logging.license
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/licenses/dom4j.license b/licenses/dom4j.license
new file mode 100644
index 0000000..a90650a
--- /dev/null
+++ b/licenses/dom4j.license
@@ -0,0 +1,40 @@
+Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
+
+Redistribution and use of this software and associated documentation
+("Software"), with or without modification, are permitted provided
+that the following conditions are met:
+
+1. Redistributions of source code must retain copyright
+   statements and notices.  Redistributions must also contain a
+   copy of this document.
+ 
+2. Redistributions in binary form must reproduce the
+   above copyright notice, this list of conditions and the
+   following disclaimer in the documentation and/or other
+   materials provided with the distribution.
+ 
+3. The name "DOM4J" must not be used to endorse or promote
+   products derived from this Software without prior written
+   permission of MetaStuff, Ltd.  For written permission,
+   please contact dom4j-info at metastuff.com.
+ 
+4. Products derived from this Software may not be called "DOM4J"
+   nor may "DOM4J" appear in their names without prior written
+   permission of MetaStuff, Ltd. DOM4J is a registered
+   trademark of MetaStuff, Ltd.
+ 
+5. Due credit should be given to the DOM4J Project - 
+   http://www.dom4j.org
+ 
+THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f928eeb
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,386 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>edu.vt.middleware</groupId>
+  <artifactId>vt-ldap</artifactId>
+  <packaging>jar</packaging>
+  <version>3.3.7</version>
+  <name>VT LDAP Libraries</name>
+  <description>Library for performing common LDAP operations</description>
+  <url>http://code.google.com/p/vt-middleware/wiki/vtldap</url>
+  <issueManagement>
+    <system>Google Code</system>
+    <url>http://code.google.com/p/vt-middleware/issues/list</url>
+  </issueManagement>
+  <mailingLists>
+    <mailingList>
+      <name>vt-middleware-users</name>
+      <subscribe>vt-middleware-users+subscribe at googlegroups.com</subscribe>
+      <unsubscribe>vt-middleware-users+unsubscribe at googlegroups.com</unsubscribe>
+      <post>vt-middleware-users at googlegroups.com</post>
+      <archive>http://groups.google.com/group/vt-middleware-users</archive>
+    </mailingList>
+    <mailingList>
+      <name>vt-middleware-dev</name>
+      <subscribe>vt-middleware-dev+subscribe at googlegroups.com</subscribe>
+      <unsubscribe>vt-middleware-dev+unsubscribe at googlegroups.com</unsubscribe>
+      <post>vt-middleware-dev at googlegroups.com</post>
+      <archive>http://groups.google.com/group/vt-middleware-dev</archive>
+    </mailingList>
+  </mailingLists>
+  <licenses>
+    <license>
+      <name>Apache 2</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+    </license>
+    <license>
+      <name>GNU Lesser General Public License</name>
+      <url>http://www.gnu.org/licenses/lgpl-3.0.txt</url>
+    </license>
+  </licenses>
+  <scm>
+    <connection>scm:svn:https://vt-middleware.googlecode.com/svn/vt-ldap/trunk</connection>
+    <url>http://vt-middleware.googlecode.com/svn/vt-ldap/trunk</url>
+  </scm>
+  <developers>
+    <developer>
+      <id>dfisher</id>
+      <name>Daniel Fisher</name>
+      <email>dfisher at vt.edu</email>
+      <organization>Virginia Tech</organization>
+      <organizationUrl>http://www.vt.edu</organizationUrl>
+      <roles>
+        <role>developer</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>marvin.addison</id>
+      <name>Marvin Addison</name>
+      <email>serac at vt.edu</email>
+      <organization>Virginia Tech</organization>
+      <organizationUrl>http://www.vt.edu</organizationUrl>
+      <roles>
+        <role>developer</role>
+      </roles>
+    </developer>
+  </developers>
+
+  <properties>
+    <project.build.sourceEncoding>iso-8859-1</project.build.sourceEncoding>
+    <checkstyle.dir>${basedir}/src/main/checkstyle</checkstyle.dir>
+    <testng.dir>${basedir}/src/test/testng</testng.dir>
+    <assembly.dir>${basedir}/src/main/assembly</assembly.dir>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.4</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-cli</groupId>
+      <artifactId>commons-cli</artifactId>
+      <version>1.2</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.1.3</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-pool</groupId>
+      <artifactId>commons-pool</artifactId>
+      <version>1.5.4</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>dom4j</groupId>
+      <artifactId>dom4j</artifactId>
+      <version>1.6.1</version>
+      <exclusions>
+        <exclusion>
+          <groupId>xml-apis</groupId>
+          <artifactId>xml-apis</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>jaxen</groupId>
+      <artifactId>jaxen</artifactId>
+      <version>1.1.4</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>servlet-api</artifactId>
+      <version>2.4</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.testng</groupId>
+      <artifactId>testng</artifactId>
+      <version>5.14</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <version>1.2.17</version>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>com.sun.jmx</groupId>
+          <artifactId>jmxri</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.sun.jdmk</groupId>
+          <artifactId>jmxtools</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>javax.jms</groupId>
+          <artifactId>jms</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>javax.mail</groupId>
+          <artifactId>mail</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>httpunit</groupId>
+      <artifactId>httpunit</artifactId>
+      <version>1.6.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-context</artifactId>
+      <version>3.2.3.RELEASE</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <testResources>
+      <testResource>
+        <directory>src/test/resources</directory>
+        <filtering>true</filtering>
+      </testResource>
+    </testResources>
+    <plugins>
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>copy-info</id>
+            <phase>validate</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>${basedir}/target/package-info</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>${basedir}</directory>
+                  <filtering>true</filtering>
+                  <includes>
+                    <include>README*</include>
+                  </includes>
+                </resource>
+                <resource>
+                  <directory>${basedir}</directory>
+                  <filtering>false</filtering>
+                  <includes>
+                    <include>LICENSE*</include>
+                    <include>NOTICE*</include>
+                    <include>CHANGELOG*</include>
+                    <include>pom.xml</include>
+                  </includes>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>copy-scripts</id>
+            <phase>validate</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>${basedir}/target/bin</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>bin</directory>
+                  <filtering>true</filtering>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <fork>true</fork>
+          <debug>true</debug>
+          <showDeprecation>true</showDeprecation>
+          <showWarnings>true</showWarnings>
+          <compilerArgument>-Xlint:unchecked</compilerArgument>
+          <source>1.5</source>
+          <target>1.5</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <version>2.5</version>
+        <configuration>
+          <configLocation>${checkstyle.dir}/checkstyle_checks.xml</configLocation>
+          <headerLocation>${checkstyle.dir}/checkstyle_header</headerLocation>
+          <suppressionsLocation>${checkstyle.dir}/suppressions.xml</suppressionsLocation>
+          <includeTestSourceDirectory>true</includeTestSourceDirectory>
+          <failsOnError>false</failsOnError>
+          <outputFileFormat>plain</outputFileFormat>
+        </configuration>
+        <executions>
+          <execution>
+            <id>checkstyle</id>
+            <phase>compile</phase>
+            <goals>
+              <goal>checkstyle</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <argLine>-Xms1536m -Xmx1536m -XX:PermSize=512m -XX:MaxPermSize=512m</argLine>
+          <!-- tests are currently host specific and require authorization -->
+          <skipTests>true</skipTests>
+          <suiteXmlFiles>
+            <suiteXmlFile>${testng.dir}/testng.xml</suiteXmlFile>
+          </suiteXmlFiles>
+          <!-- sometimes useful if testng runs out of memory
+          <disableXmlReport>true</disableXmlReport>
+          -->
+          <systemProperties>
+            <property>
+              <name>log4j.configuration</name>
+              <value>log4j.xml</value>
+            </property>
+          </systemProperties>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <configuration>
+          <links>
+            <link>http://java.sun.com/j2se/1.5.0/docs/api</link>
+          </links>
+          <bottom><![CDATA[<i>Copyright © 2003-2010 Virginia Tech. All Rights Reserved.</i>]]></bottom>
+        </configuration>
+        <executions>
+          <execution>
+            <id>javadoc</id>
+            <phase>package</phase>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>source</id>
+            <phase>package</phase>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>jar</id>
+            <phase>package</phase>
+            <goals>
+              <goal>test-jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <configuration>
+          <appendAssemblyId>true</appendAssemblyId>
+          <descriptors>
+            <descriptor>${assembly.dir}/vt-middleware.xml</descriptor>
+          </descriptors>
+        </configuration>
+        <executions>
+          <execution>
+            <id>assembly</id>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+    <extensions>
+      <extension>
+        <groupId>org.jvnet.wagon-svn</groupId>
+        <artifactId>wagon-svn</artifactId>
+        <version>1.9</version>
+      </extension>
+    </extensions>
+  </build>
+  <profiles>
+    <profile>
+      <id>sign-artifacts</id>
+      <activation>
+        <property>
+          <name>sign</name>
+          <value>true</value>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-gpg-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>sign-artifacts</id>
+                <phase>package</phase>
+                <goals>
+                  <goal>sign</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/properties/krb5_jaas.config b/properties/krb5_jaas.config
new file mode 100644
index 0000000..08b3557
--- /dev/null
+++ b/properties/krb5_jaas.config
@@ -0,0 +1,11 @@
+com.sun.security.jgss.initiate {
+  com.sun.security.auth.module.Krb5LoginModule required
+    doNotPrompt="true"
+    debug="true"
+    renewTGT="true"
+    principal="dfisher"
+    useTicketCache="true"
+    ticketCache="/tmp/krb5cc_dfisher"
+    useKeyTab="true"
+    keyTab="/home/dfisher/krb5.keytab";
+};
diff --git a/properties/ldap.properties b/properties/ldap.properties
new file mode 100644
index 0000000..7354b2c
--- /dev/null
+++ b/properties/ldap.properties
@@ -0,0 +1,122 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# fully qualified class name of the context factory that JNDI should use
+# default value is 'com.sun.jndi.ldap.LdapCtxFactory'
+#edu.vt.middleware.ldap.contextFactory=
+
+# fully qualified class name which implements javax.net.ssl.SSLSocketFactory
+#edu.vt.middleware.ldap.sslSocketFactory=
+
+# fully qualified class name which implements javax.net.ssl.HostnameVerifier
+#edu.vt.middleware.ldap.hostnameVerifier=
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://directory.vt.edu:389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=People,dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+#edu.vt.middleware.ldap.bindDn=cn=manager,ou=Services,dc=vt,dc=edu
+
+# credential for the bind DN
+#edu.vt.middleware.ldap.bindCredential=manager_password
+
+# LDAP authentication mechanism
+# default value is 'simple'
+#edu.vt.middleware.ldap.authtype=
+
+# require an authoritative source, this value must be either 'true' or 'false'
+#edu.vt.middleware.ldap.authoritative=
+
+# sets the amount of time in milliseconds that search operations will block
+#edu.vt.middleware.ldap.timeLimit=
+
+# sets the amount of time in milliseconds that connect operations will block
+#edu.vt.middleware.ldap.timeout=
+
+# sets the maximum number of entries that search operations will return
+#edu.vt.middleware.ldap.countLimit=
+
+# sets the batch size to use when returning results
+# default value is '-1'
+#edu.vt.middleware.ldap.batchSize=
+
+# sets the DNS url to use for hostname resolution
+# example is 'dns://somehost/wiz.com'
+#edu.vt.middleware.ldap.dnsUrl=
+
+# sets the preferred language
+# default value is determined by the service provider
+#edu.vt.middleware.ldap.language=
+
+# specifies how referrals should be handled
+# must be one of 'throw', 'ignore', or 'follow'
+#edu.vt.middleware.ldap.referral=
+
+# specifies how aliases should be handled
+# must be one of 'always', 'never', 'finding', or 'searching'
+#edu.vt.middleware.ldap.derefAliases=
+
+# specifies additional attributes which should be treated as binary
+# attribute names should be space delimited
+edu.vt.middleware.ldap.binaryAttributes=userSMIMECertificate
+
+# only return attribute type names, this value must be either 'true' or 'false'
+#edu.vt.middleware.ldap.typesOnly=
+
+# whether SSL should be used for LDAP connections
+# default value is 'false'
+#edu.vt.middleware.ldap.ssl=
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+#edu.vt.middleware.ldap.tls=
+
+## LDAP AUTHENTICATOR CONFIG ##
+
+# can be used to override any of the previous properties
+
+# fully qualified class name which implements javax.net.ssl.SSLSocketFactory
+#edu.vt.middleware.ldap.auth.sslSocketFactory=
+
+# fully qualified class name which implements javax.net.ssl.HostnameVerifier
+#edu.vt.middleware.ldap.auth.hostnameVerifier=
+
+# hostname and optional port of your LDAP
+edu.vt.middleware.ldap.auth.ldapUrl=ldap://authn.directory.vt.edu:389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.auth.baseDn=ou=People,dc=vt,dc=edu
+
+# LDAP authentication mechanism
+# default value is 'simple'
+#edu.vt.middleware.ldap.auth.authtype=
+
+# LDAP field which contains user identifier
+edu.vt.middleware.ldap.auth.userField=uupid
+
+# whether the authentication dn should be constructed or looked up in the LDAP
+edu.vt.middleware.ldap.auth.constructDn=false
+
+# whether the authentication dn should be searched for over the entire base
+edu.vt.middleware.ldap.auth.subtreeSearch=false
+
+# whether authentication credentials should be logged
+# default value is 'false'
+#edu.vt.middleware.ldap.auth.logCredentials=
+
+# whether SSL should be used for LDAP connections
+# default value is 'false'
+#edu.vt.middleware.ldap.auth.ssl=
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.auth.tls=true
+
+# ldap filter to use for performing authorization
+#edu.vt.middleware.ldap.auth.authorizationFilter=(&(eduPersonAffiliation=VT-ALUM)(eduPersonAffiliation=VT-EMPLOYEE))
diff --git a/properties/ldap_jaas.config b/properties/ldap_jaas.config
new file mode 100644
index 0000000..9dccf02
--- /dev/null
+++ b/properties/ldap_jaas.config
@@ -0,0 +1,8 @@
+vt-ldap {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    ldapUrl="ldap://authn.directory.vt.edu:389"
+    baseDn="ou=people,dc=vt,dc=edu"
+    tls="true"
+    userFilter="(uupid={0})"
+    userRoleAttribute="eduPersonAffiliation,groupMembership";
+};
diff --git a/src/main/assembly/vt-middleware.xml b/src/main/assembly/vt-middleware.xml
new file mode 100644
index 0000000..2386fdd
--- /dev/null
+++ b/src/main/assembly/vt-middleware.xml
@@ -0,0 +1,74 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<assembly>
+  <id>dist</id>
+  <formats>
+    <format>tar.gz</format>
+    <format>zip</format>
+  </formats>
+  <dependencySets>
+    <dependencySet>
+      <useProjectArtifact>false</useProjectArtifact>
+      <outputDirectory>lib</outputDirectory>
+      <scope>runtime</scope>
+      <includes>
+        <include>*:jar</include>
+      </includes>
+    </dependencySet>
+  </dependencySets>
+  <fileSets>
+    <fileSet>
+      <directory>target/package-info</directory>
+      <outputDirectory>/</outputDirectory>
+      <includes>
+        <include>**</include>
+      </includes>
+      <useDefaultExcludes>true</useDefaultExcludes>
+    </fileSet>
+    <fileSet>
+      <directory>src</directory>
+      <useDefaultExcludes>true</useDefaultExcludes>
+    </fileSet>
+    <fileSet>
+      <directory>properties</directory>
+      <useDefaultExcludes>true</useDefaultExcludes>
+    </fileSet>
+    <fileSet>
+      <directory>licenses</directory>
+      <useDefaultExcludes>true</useDefaultExcludes>
+    </fileSet>
+    <fileSet>
+      <directory>dict</directory>
+      <useDefaultExcludes>true</useDefaultExcludes>
+    </fileSet>
+    <fileSet>
+      <directory>target/bin</directory>
+      <outputDirectory>bin</outputDirectory>
+      <fileMode>0755</fileMode>
+      <useDefaultExcludes>true</useDefaultExcludes>
+      <excludes>
+        <exclude>*.bat</exclude>
+      </excludes>
+    </fileSet>
+    <fileSet>
+      <directory>target/bin</directory>
+      <outputDirectory>bin</outputDirectory>
+      <useDefaultExcludes>true</useDefaultExcludes>
+      <includes>
+        <include>*.bat</include>
+      </includes>
+    </fileSet>
+    <fileSet>
+      <directory>target</directory>
+      <outputDirectory>jars</outputDirectory>
+      <includes>
+        <include>${project.build.finalName}*.jar</include>
+      </includes>
+      <useDefaultExcludes>true</useDefaultExcludes>
+    </fileSet>
+    <fileSet>
+      <directory>target/site/apidocs</directory>
+      <outputDirectory>docs/apidocs</outputDirectory>
+      <useDefaultExcludes>true</useDefaultExcludes>
+    </fileSet>
+  </fileSets>
+</assembly>
diff --git a/src/main/checkstyle/checkstyle_checks.xml b/src/main/checkstyle/checkstyle_checks.xml
new file mode 100644
index 0000000..051c88f
--- /dev/null
+++ b/src/main/checkstyle/checkstyle_checks.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC
+    "-//Puppy Crawl//DTD Check Configuration 1.1//EN"
+    "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<module name="Checker">
+
+  <!-- limit source file size -->
+  <module name="FileLength">
+    <property name="max" value="2000" />
+  </module>
+
+  <!-- forbid tab characters in source files -->
+  <module name="FileTabCharacter"/>
+
+  <module name="NewlineAtEndOfFile"/>
+  <module name="TreeWalker">
+    <property name="tabWidth" value="2"/>
+
+    <!-- javadocs -->
+    <module name="JavadocType">
+      <property name="authorFormat" value="\S+"/>
+      <property name="versionFormat" value="\S+"/>
+    </module>
+    <module name="JavadocMethod">
+      <property name="allowThrowsTagsForSubclasses" value="true"/>
+      <property name="allowUndeclaredRTE" value="true"/>
+    </module>
+    <module name="JavadocVariable"/>
+    <module name="JavadocStyle">
+      <property name="checkFirstSentence" value="false"/>
+    </module>
+
+    <!-- naming -->
+    <module name="AbstractClassName"/>
+    <module name="ClassTypeParameterName"/>
+    <module name="ConstantName"/>
+    <module name="LocalFinalVariableName"/>
+    <module name="LocalVariableName"/>
+    <module name="MemberName"/>
+    <module name="MethodName"/>
+    <module name="MethodTypeParameterName"/>
+    <module name="PackageName"/>
+    <module name="ParameterName"/>
+    <module name="StaticVariableName"/>
+    <module name="TypeName"/>
+
+    <!-- imports -->
+    <module name="AvoidStarImport"/>
+    <module name="AvoidStaticImport"/>
+    <module name="IllegalImport"/>
+    <module name="RedundantImport"/>
+    <module name="UnusedImports"/>
+    <module name="ImportOrder">
+      <property name="groups" value="java,javax"/>
+      <property name="ordered" value="true"/>
+    </module>
+
+    <!-- sizes -->
+    <module name="LineLength">
+      <property name="ignorePattern" value="(^.*\$.*\$.*$)|(^import .*)"/>
+      <property name="max" value="80"/>
+    </module>
+    <module name="MethodLength">
+      <property name="max" value="300"/>
+    </module>
+    <module name="AnonInnerLength"/>
+    <module name="ParameterNumber"/>
+
+    <!-- whitespace -->
+    <module name="GenericWhitespace"/>
+    <module name="EmptyForInitializerPad"/>
+    <module name="EmptyForIteratorPad"/>
+    <module name="MethodParamPad"/>
+    <module name="NoWhitespaceAfter"/>
+    <module name="NoWhitespaceBefore">
+      <property name="allowLineBreaks" value="true"/>
+      <property name="tokens"
+                value="SEMI,DOT,POST_DEC,POST_INC,PLUS"/>
+    </module>
+    <module name="OperatorWrap">
+      <property name="option" value="eol"/>
+      <property name="tokens"
+                value="BAND,BOR,BSR,BXOR,DIV,EQUAL,GE,GT,LAND,LE,
+                       LITERAL_INSTANCEOF,LOR,LT,MINUS,MOD,NOT_EQUAL,PLUS,
+                       SL,SR,STAR"/>
+    </module>
+    <module name="ParenPad"/>
+    <module name="TypecastParenPad"/>
+    <module name="WhitespaceAfter"/>
+    <module name="WhitespaceAround">
+      <property name="tokens"
+                value="ASSIGN,BAND,BAND_ASSIGN,BOR,BOR_ASSIGN,BSR,BSR_ASSIGN,
+                       BXOR,BXOR_ASSIGN,COLON,DIV,DIV_ASSIGN,EQUAL,GE,GT,LAND,
+                       LE,LITERAL_ASSERT,LITERAL_CATCH,LITERAL_DO,
+                       LITERAL_ELSE,LITERAL_FINALLY,LITERAL_FOR,LITERAL_IF,
+                       LITERAL_RETURN,LITERAL_SYNCHRONIZED,LITERAL_TRY,
+                       LITERAL_WHILE,LOR,LT,MINUS,MINUS_ASSIGN,MOD,MOD_ASSIGN,
+                       NOT_EQUAL,PLUS_ASSIGN,QUESTION,SL,
+                       SL_ASSIGN,SR,SR_ASSIGN,STAR,STAR_ASSIGN"/>
+    </module>
+
+    <!-- modifiers -->
+    <module name="ModifierOrder"/>
+    <module name="RedundantModifier"/>
+
+    <!-- blocks -->
+    <module name="EmptyBlock"/>
+    <module name="LeftCurly">
+      <property name="option" value="nl"/>
+      <property name="tokens"
+                value="CLASS_DEF,CTOR_DEF,INTERFACE_DEF,METHOD_DEF"/>
+    </module>
+    <module name="NeedBraces"/>
+    <module name="RightCurly"/>
+    <module name="AvoidNestedBlocks"/>
+
+    <!-- coding -->
+    <module name="ArrayTrailingComma"/>
+    <module name="CovariantEquals"/>
+    <module name="DoubleCheckedLocking"/>
+    <module name="EmptyStatement"/>
+    <module name="EqualsAvoidNull"/>
+    <module name="EqualsHashCode"/>
+    <module name="FinalLocalVariable"/>
+    <!--
+    <module name="HiddenField"/>
+    -->
+    <module name="IllegalInstantiation">
+      <property name="classes" value="java.lang.Boolean"/>
+    </module>
+    <module name="InnerAssignment"/>
+    <module name="MagicNumber"/>
+    <module name="MissingSwitchDefault"/>
+    <module name="RedundantThrows"/>
+    <module name="SimplifyBooleanExpression"/>
+    <module name="SimplifyBooleanReturn"/>
+    <module name="StringLiteralEquality"/>
+    <module name="NestedIfDepth">
+      <property name="max" value="5"/>
+    </module>
+    <module name="NestedTryDepth">
+      <property name="max" value="5"/>
+    </module>
+    <module name="SuperClone"/>
+    <module name="SuperFinalize"/>
+    <module name="PackageDeclaration"/>
+    <module name="ReturnCount"/>
+    <module name="IllegalType">
+      <property name="illegalClassNames"
+                value="java.util.GregorianCalendar, java.util.Hashtable,
+                       java.util.HashSet, java.util.HashMap,
+                       java.util.ArrayList, java.util.LinkedList,
+                       java.util.LinkedHashMap, java.util.LinkedHashSet,
+                       java.util.TreeSet, java.util.TreeMap, java.util.Vector"/>
+    </module>
+    <module name="DeclarationOrder"/>
+    <module name="ParameterAssignment"/>
+    <module name="ExplicitInitialization"/>
+    <module name="DefaultComesLast"/>
+    <module name="FallThrough"/>
+    <module name="MultipleVariableDeclarations"/>
+    <module name="RequireThis">
+      <property name="checkFields" value="false"/>
+      <property name="checkMethods" value="false"/>
+    </module>
+    <module name="UnnecessaryParentheses"/>
+
+    <!-- design -->
+    <module name="VisibilityModifier">
+      <property name="protectedAllowed" value="true"/>
+    </module>
+    <module name="FinalClass"/>
+    <module name="InterfaceIsType"/>
+    <module name="HideUtilityClassConstructor"/>
+    <module name="MutableException"/>
+    <module name="ThrowsCount">
+      <property name="max" value="2"/>
+    </module>
+
+    <!-- prevent trailing whitespace -->
+    <module name="Regexp">
+      <property name="format" value="[ \t]+$"/>
+      <property name="illegalPattern" value="true"/>
+      <property name="message" value="Trailing whitespace"/>
+    </module>
+
+    <!-- misc -->
+    <module name="UpperEll"/>
+    <module name="ArrayTypeStyle"/>
+    <module name="FinalParameters"/>
+    <module name="Indentation">
+      <property name="basicOffset" value="2"/>
+      <property name="caseIndent" value="0"/>
+    </module>
+    <module name="TrailingComment"/>
+
+  </module>
+</module>
diff --git a/src/main/checkstyle/checkstyle_header b/src/main/checkstyle/checkstyle_header
new file mode 100644
index 0000000..d8c79ee
--- /dev/null
+++ b/src/main/checkstyle/checkstyle_header
@@ -0,0 +1,13 @@
+/\*
+  \$Id*
+
+  Copyright \(C\) \d\d\d\d Virginia Tech*
+  All rights reserved\.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  *
+  Email:   *\@*\.*
+  Version: \$Revision*
+  Updated: \$Date*
+\*/
diff --git a/src/main/checkstyle/suppressions.xml b/src/main/checkstyle/suppressions.xml
new file mode 100644
index 0000000..c548080
--- /dev/null
+++ b/src/main/checkstyle/suppressions.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suppressions PUBLIC
+    "-//Puppy Crawl//DTD Suppressions 1.0//EN"
+    "http://www.puppycrawl.com/dtds/suppressions_1_0.dtd">
+
+<suppressions>
+  <!-- classloader problem with custom exception -->
+  <suppress checks=".*"
+            files=".*\.java"
+            lines="0"/>
+
+  <!-- nothing magic about test data -->
+  <suppress checks="MagicNumber"
+            files=".*Test\.java" />
+</suppressions>
diff --git a/src/main/java/edu/vt/middleware/ldap/AbstractCli.java b/src/main/java/edu/vt/middleware/ldap/AbstractCli.java
new file mode 100644
index 0000000..6c14f68
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/AbstractCli.java
@@ -0,0 +1,278 @@
+/*
+  $Id: AbstractCli.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import edu.vt.middleware.ldap.props.LdapConfigPropertyInvoker;
+import edu.vt.middleware.ldap.props.LdapProperties;
+import edu.vt.middleware.ldap.props.PropertyConfig;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Abstract base class for all CLI handlers.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public abstract class AbstractCli
+{
+
+  /** Option to print usage. */
+  protected static final String OPT_HELP = "help";
+
+  /** Option for ldap trace. */
+  protected static final String OPT_TRACE = "trace";
+
+  /** Option for loading ldap configuration from properties. */
+  protected static final String OPT_USE_PROPERTIES = "useProperties";
+
+  /** Option for dsmlv1 output. */
+  protected static final String OPT_DSMLV1 = "dsmlv1";
+
+  /** Option for dsmlv2 output. */
+  protected static final String OPT_DSMLV2 = "dsmlv2";
+
+  /** List of command options. */
+  protected List<String> opts = new ArrayList<String>();
+
+  /** Log. */
+  protected final Log logger = LogFactory.getLog(getClass());
+
+  /** Command line options. */
+  protected Options options = new Options();
+
+  /** Whether to output dsml version 1, the default is ldif. */
+  protected boolean outputDsmlv1;
+
+  /** Whether to output dsml version 2, the default is ldif. */
+  protected boolean outputDsmlv2;
+
+
+  /** Default constructor. */
+  public AbstractCli()
+  {
+    this.opts.add(OPT_HELP);
+    this.opts.add(OPT_TRACE);
+    this.opts.add(OPT_USE_PROPERTIES);
+    this.opts.add(OPT_DSMLV1);
+    this.opts.add(OPT_DSMLV2);
+  }
+
+
+  /**
+   * Parses command line options and invokes the proper handler to perform the
+   * requested action, or the default action if no action is specified.
+   *
+   * @param  args  Command line arguments.
+   */
+  public final void performAction(final String[] args)
+  {
+    initOptions();
+    try {
+      if (args.length > 0) {
+        final CommandLineParser parser = new GnuParser();
+        final CommandLine line = parser.parse(options, args);
+        dispatch(line);
+      } else {
+        printExamples();
+      }
+    } catch (ParseException pex) {
+      System.err.println(
+        "Failed parsing command arguments: " + pex.getMessage());
+    } catch (IllegalArgumentException iaex) {
+      String msg = "Operation failed: " + iaex.getMessage();
+      if (iaex.getCause() != null) {
+        msg += " Underlying reason: " + iaex.getCause().getMessage();
+      }
+      System.err.println(msg);
+    } catch (Exception ex) {
+      System.err.println("Operation failed:");
+      ex.printStackTrace(System.err);
+    }
+  }
+
+
+  /** Initialize CLI options. */
+  protected abstract void initOptions();
+
+
+  /**
+   * Initialize CLI options with the supplied invoker.
+   *
+   * @param  invoker  <code>PropertyInvoker</code>
+   */
+  protected void initOptions(final LdapConfigPropertyInvoker invoker)
+  {
+    final Map<String, String> args = this.getArgs();
+    for (String s : invoker.getProperties()) {
+      final String arg = s.substring(s.lastIndexOf(".") + 1, s.length());
+      if (args.containsKey(arg)) {
+        options.addOption(new Option(arg, true, args.get(arg)));
+      }
+    }
+    options.addOption(new Option(OPT_HELP, false, "display all options"));
+    options.addOption(
+      new Option(OPT_TRACE, false, "print ASN.1 BER packets to System.out"));
+    options.addOption(
+      new Option(
+        OPT_USE_PROPERTIES,
+        false,
+        "load options from the default properties file"));
+    options.addOption(
+      new Option(OPT_DSMLV1, false, "output results in DSML v1"));
+    options.addOption(
+      new Option(OPT_DSMLV2, false, "output results in DSML v2"));
+  }
+
+
+  /**
+   * Gets the name of the command for which this class provides a CLI interface.
+   *
+   * @return  Name of CLI command.
+   */
+  protected abstract String getCommandName();
+
+
+  /**
+   * Dispatch command line data to the handler that can perform the operation
+   * requested on the command line.
+   *
+   * @param  line  Parsed command line arguments container.
+   *
+   * @throws  Exception  On errors thrown by handler.
+   */
+  protected abstract void dispatch(final CommandLine line)
+    throws Exception;
+
+
+  /** Prints CLI help text. */
+  protected void printHelp()
+  {
+    final HelpFormatter formatter = new HelpFormatter();
+    formatter.printHelp(getCommandName(), options);
+  }
+
+
+  /** Prints CLI usage examples. */
+  protected void printExamples()
+  {
+    final String fullName = getClass().getName();
+    final String name = fullName.substring(fullName.lastIndexOf('.') + 1);
+    final InputStream in = getClass().getResourceAsStream(name + ".examples");
+    if (in != null) {
+      final BufferedReader reader = new BufferedReader(
+        new InputStreamReader(in));
+      try {
+        System.out.println();
+
+        String line = null;
+        while ((line = reader.readLine()) != null) {
+          System.out.println(line);
+        }
+      } catch (IOException e) {
+        System.err.println("Error reading examples from resource stream.");
+      } finally {
+        try {
+          reader.close();
+        } catch (IOException ex) {
+          System.err.println("Error closing example resource stream.");
+        }
+        System.out.println();
+      }
+    } else {
+      System.out.println("No usage examples available for " + getCommandName());
+    }
+  }
+
+
+  /**
+   * Returns the command line arguments for this cli.
+   *
+   * @return  map of arg name to description
+   */
+  protected Map<String, String> getArgs()
+  {
+    final Map<String, String> args = new HashMap<String, String>();
+    final String fullName = getClass().getName();
+    final String name = fullName.substring(fullName.lastIndexOf('.') + 1);
+    final InputStream in = getClass().getResourceAsStream(name + ".args");
+    if (in != null) {
+      final BufferedReader reader = new BufferedReader(
+        new InputStreamReader(in));
+      try {
+        System.out.println();
+
+        String line = null;
+        while ((line = reader.readLine()) != null) {
+          final String[] s = line.split(":");
+          if (s.length > 1) {
+            args.put(s[0], s[1]);
+          }
+        }
+      } catch (IOException e) {
+        System.err.println("Error reading arguments from resource stream.");
+      } finally {
+        try {
+          reader.close();
+        } catch (IOException ex) {
+          System.err.println("Error closing arguments resource stream.");
+        }
+        System.out.println();
+      }
+    } else {
+      System.out.println("No arguments available for " + getCommandName());
+    }
+    return args;
+  }
+
+
+  /**
+   * Initialize the supplied config with command line options.
+   *
+   * @param  config  property config to configure
+   * @param  line  Parsed command line arguments container.
+   *
+   * @throws  Exception  On errors thrown by handler.
+   */
+  protected void initLdapProperties(
+    final PropertyConfig config,
+    final CommandLine line)
+    throws Exception
+  {
+    final LdapProperties ldapProperties = new LdapProperties(config);
+    for (Option o : line.getOptions()) {
+      if (o.getOpt().equals(OPT_USE_PROPERTIES)) {
+        ldapProperties.useDefaultPropertiesFile();
+      } else if (!this.opts.contains(o.getOpt())) {
+        ldapProperties.setProperty(o.getOpt(), o.getValue());
+      }
+    }
+    ldapProperties.configure();
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/AbstractLdap.java b/src/main/java/edu/vt/middleware/ldap/AbstractLdap.java
new file mode 100644
index 0000000..3d5973b
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/AbstractLdap.java
@@ -0,0 +1,1166 @@
+/*
+  $Id: AbstractLdap.java 1440 2010-06-27 16:41:34Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1440 $
+  Updated: $Date: 2010-06-27 17:41:34 +0100 (Sun, 27 Jun 2010) $
+*/
+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: 1440 $ $Date: 2010-06-27 17:41:34 +0100 (Sun, 27 Jun 2010) $
+ */
+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 at 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();
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/AttributesFactory.java b/src/main/java/edu/vt/middleware/ldap/AttributesFactory.java
new file mode 100644
index 0000000..9db5830
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/AttributesFactory.java
@@ -0,0 +1,192 @@
+/*
+  $Id: AttributesFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+
+/**
+ * <code>AttributesFactory</code> provides convenience methods for creating
+ * <code>Attributes</code> and <code>Attribute</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class AttributesFactory
+{
+
+  /** Default constructor. */
+  private AttributesFactory() {}
+
+
+  /**
+   * Creates a new <code>Attributes</code> with the supplied name. Attributes
+   * will be case-insensitive.
+   *
+   * @param  name  of the attribute
+   *
+   * @return  <code>Attributes</code>
+   */
+  public static Attributes createAttributes(final String name)
+  {
+    return createAttributes(name, LdapConstants.DEFAULT_IGNORE_CASE);
+  }
+
+
+  /**
+   * Creates a new <code>Attributes</code> with the supplied name.
+   *
+   * @param  name  of the attribute
+   * @param  ignoreCase  whether to ignore the case of attribute values
+   *
+   * @return  <code>Attributes</code>
+   */
+  public static Attributes createAttributes(
+    final String name,
+    final boolean ignoreCase)
+  {
+    return createAttributes(name, null, ignoreCase);
+  }
+
+
+  /**
+   * Creates a new <code>Attributes</code> with the supplied name and value.
+   * Attributes will be case-insensitive.
+   *
+   * @param  name  of the attribute
+   * @param  value  of the attribute
+   *
+   * @return  <code>Attributes</code>
+   */
+  public static Attributes createAttributes(
+    final String name,
+    final Object value)
+  {
+    return createAttributes(name, value, LdapConstants.DEFAULT_IGNORE_CASE);
+  }
+
+
+  /**
+   * Creates a new <code>Attributes</code> with the supplied name and value.
+   *
+   * @param  name  of the attribute
+   * @param  value  of the attribute
+   * @param  ignoreCase  whether to ignore the case of attribute values
+   *
+   * @return  <code>Attributes</code>
+   */
+  public static Attributes createAttributes(
+    final String name,
+    final Object value,
+    final boolean ignoreCase)
+  {
+    if (value == null) {
+      return createAttributes(name, null, ignoreCase);
+    } else {
+      return createAttributes(name, new Object[] {value}, ignoreCase);
+    }
+  }
+
+
+  /**
+   * Creates a new <code>Attributes</code> with the supplied name and values.
+   * Attributes will be case-insensitive.
+   *
+   * @param  name  of the attribute
+   * @param  values  of the attribute
+   *
+   * @return  <code>Attributes</code>
+   */
+  public static Attributes createAttributes(
+    final String name,
+    final Object[] values)
+  {
+    return createAttributes(name, values, LdapConstants.DEFAULT_IGNORE_CASE);
+  }
+
+
+  /**
+   * Creates a new <code>Attributes</code> with the supplied name and values.
+   *
+   * @param  name  of the attribute
+   * @param  values  of the attribute
+   * @param  ignoreCase  whether to ignore the case of attribute values
+   *
+   * @return  <code>Attributes</code>
+   */
+  public static Attributes createAttributes(
+    final String name,
+    final Object[] values,
+    final boolean ignoreCase)
+  {
+    final Attributes attrs = new BasicAttributes(ignoreCase);
+    attrs.put(createAttribute(name, values));
+    return attrs;
+  }
+
+
+  /**
+   * Creates a new <code>Attribute</code> with the supplied name.
+   *
+   * @param  name  of the attribute
+   *
+   * @return  <code>Attribute</code>
+   */
+  public static Attribute createAttribute(final String name)
+  {
+    return createAttribute(name, null);
+  }
+
+
+  /**
+   * Creates a new <code>Attribute</code> with the supplied name and value.
+   *
+   * @param  name  of the attribute
+   * @param  value  of the attribute
+   *
+   * @return  <code>Attribute</code>
+   */
+  public static Attribute createAttribute(final String name, final Object value)
+  {
+    if (value == null) {
+      return createAttribute(name, null);
+    } else {
+      return createAttribute(name, new Object[] {value});
+    }
+  }
+
+
+  /**
+   * Creates a new <code>Attribute</code> with the supplied name and values.
+   *
+   * @param  name  of the attribute
+   * @param  values  of the attribute
+   *
+   * @return  <code>Attribute</code>
+   */
+  public static Attribute createAttribute(
+    final String name,
+    final Object[] values)
+  {
+    final Attribute attr = new BasicAttribute(name);
+    if (values != null) {
+      for (Object o : values) {
+        attr.add(o);
+      }
+    }
+    return attr;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/BaseLdap.java b/src/main/java/edu/vt/middleware/ldap/BaseLdap.java
new file mode 100644
index 0000000..ce1c87a
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/BaseLdap.java
@@ -0,0 +1,52 @@
+/*
+  $Id: BaseLdap.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import javax.naming.NamingException;
+
+/**
+ * <code>BaseLdap</code> provides a base interface for all ldap implementations.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface BaseLdap
+{
+
+
+  /**
+   * This will establish a connection to the ldap.
+   *
+   * @return  <code>boolean</code> - whether the connection was successfull
+   *
+   * @throws  NamingException  if the LDAP cannot be reached
+   */
+  boolean connect()
+    throws NamingException;
+
+
+  /**
+   * This will close the connection to the LDAP and establish a new connection.
+   *
+   * @return  <code>boolean</code> - whether the connection was successfull
+   *
+   * @throws  NamingException  if the LDAP cannot be reached
+   */
+  boolean reconnect()
+    throws NamingException;
+
+
+  /** This will close the connection to the LDAP. */
+  void close();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/Ldap.java b/src/main/java/edu/vt/middleware/ldap/Ldap.java
new file mode 100644
index 0000000..d05a231
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/Ldap.java
@@ -0,0 +1,747 @@
+/*
+  $Id: Ldap.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.Iterator;
+import javax.naming.Binding;
+import javax.naming.NameClassPair;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+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 edu.vt.middleware.ldap.handler.AttributeHandler;
+import edu.vt.middleware.ldap.handler.ExtendedAttributeHandler;
+import edu.vt.middleware.ldap.handler.ExtendedSearchResultHandler;
+import edu.vt.middleware.ldap.handler.SearchCriteria;
+import edu.vt.middleware.ldap.handler.SearchResultHandler;
+
+/**
+ * <code>Ldap</code> contains functions for basic interaction with an LDAP.
+ * Methods are provided for connecting, binding, querying and updating.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class Ldap extends AbstractLdap<LdapConfig> implements Serializable
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -2715321533384426365L;
+
+  /**
+   * Enum to define the type of attribute modification. See {@link
+   * javax.naming.directory.DirContext}.
+   */
+  public enum AttributeModification {
+
+    /** add an attribute. */
+    ADD(DirContext.ADD_ATTRIBUTE),
+
+    /** replace an attribute. */
+    REPLACE(DirContext.REPLACE_ATTRIBUTE),
+
+    /** remove an attribute. */
+    REMOVE(DirContext.REMOVE_ATTRIBUTE);
+
+
+    /** underlying modification operation integer. */
+    private int modOp;
+
+
+    /**
+     * Creates a new <code>AttributeModification</code> with the supplied
+     * integer.
+     *
+     * @param  i  modification operation
+     */
+    AttributeModification(final int i)
+    {
+      this.modOp = i;
+    }
+
+
+    /**
+     * Returns the modification operation integer.
+     *
+     * @return  <code>int</code>
+     */
+    public int modOp()
+    {
+      return this.modOp;
+    }
+
+
+    /**
+     * Method to convert a JNDI constant value to an enum. Returns null if the
+     * supplied constant does not match a valid value.
+     *
+     * @param  i  modification operation
+     *
+     * @return  attribute modification
+     */
+    public static AttributeModification parseModificationOperation(final int i)
+    {
+      AttributeModification am = null;
+      if (ADD.modOp() == i) {
+        am = ADD;
+      } else if (REPLACE.modOp() == i) {
+        am = REPLACE;
+      } else if (REMOVE.modOp() == i) {
+        am = REMOVE;
+      }
+      return am;
+    }
+  }
+
+  /** Default constructor. */
+  public Ldap() {}
+
+
+  /**
+   * This will create a new <code>Ldap</code> with the supplied <code>
+   * LdapConfig</code>.
+   *
+   * @param  ldapConfig  <code>LdapConfig</code>
+   */
+  public Ldap(final LdapConfig ldapConfig)
+  {
+    this.setLdapConfig(ldapConfig);
+  }
+
+
+  /** {@inheritDoc} */
+  public void setLdapConfig(final LdapConfig ldapConfig)
+  {
+    super.setLdapConfig(ldapConfig);
+  }
+
+
+  /**
+   * This returns the <code>LdapConfig</code> of the <code>Ldap</code>.
+   *
+   * @return  <code>LdapConfig</code>
+   */
+  public LdapConfig getLdapConfig()
+  {
+    return this.config;
+  }
+
+
+  /**
+   * This will set the config parameters of this <code>Ldap</code> using the
+   * default properties file, which must be located in your classpath.
+   */
+  public void loadFromProperties()
+  {
+    this.setLdapConfig(LdapConfig.createFromProperties(null));
+  }
+
+
+  /**
+   * This will set the config parameters of this <code>Ldap</code> using the
+   * supplied input stream.
+   *
+   * @param  is  <code>InputStream</code>
+   */
+  public void loadFromProperties(final InputStream is)
+  {
+    this.setLdapConfig(LdapConfig.createFromProperties(is));
+  }
+
+
+  /**
+   * This will perform an LDAP compare operation with the supplied filter.
+   * {@link LdapConfig#getBaseDn()} is used as the dn to compare. See {@link
+   * #compare(String, SearchFilter)}.
+   *
+   * @param  filter  <code>SearchFilter</code> expression to use for compare
+   *
+   * @return  <code>boolean</code> - result of compare operation
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public boolean compare(final SearchFilter filter)
+    throws NamingException
+  {
+    return this.compare(this.config.getBaseDn(), filter);
+  }
+
+
+  /**
+   * This will perform an LDAP compare operation with the supplied filter and
+   * dn.
+   *
+   * @param  dn  <code>String</code> name to compare
+   * @param  filter  <code>SearchFilter</code> expression to use for compare
+   *
+   * @return  <code>boolean</code> - result of compare operation
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public boolean compare(final String dn, final SearchFilter filter)
+    throws NamingException
+  {
+    return
+      super.compare(dn, filter.getFilter(), filter.getFilterArgs().toArray());
+  }
+
+
+  /**
+   * This will query the LDAP with the supplied filter. All attributes will be
+   * returned. {@link LdapConfig#getBaseDn()} is used as the start point for
+   * searching. Search controls will be created from {@link
+   * LdapConfig#getSearchControls(String[])}. See {@link
+   * #search(String,SearchFilter,String[])}.
+   *
+   * @param  filter  <code>SearchFilter</code> expression to use for the search
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> search(final SearchFilter filter)
+    throws NamingException
+  {
+    return
+      this.search(
+        this.config.getBaseDn(),
+        filter,
+        this.config.getSearchControls(null));
+  }
+
+
+  /**
+   * This will query the LDAP with the supplied filter and return attributes.
+   * {@link LdapConfig#getBaseDn()} is used as the start point for searching.
+   * Search controls will be created from {@link
+   * LdapConfig#getSearchControls(String[])}. See {@link
+   * #search(String,SearchFilter,String[])}.
+   *
+   * @param  filter  <code>SearchFilter</code> expression to use for the search
+   * @param  retAttrs  <code>String[]</code> attributes to return
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> search(
+    final SearchFilter filter,
+    final String[] retAttrs)
+    throws NamingException
+  {
+    return
+      this.search(
+        this.config.getBaseDn(),
+        filter,
+        this.config.getSearchControls(retAttrs));
+  }
+
+
+  /**
+   * This will query the LDAP with the supplied filter and search controls.
+   * {@link LdapConfig#getBaseDn()} is used as the start point for searching.
+   * See {@link #search(String,SearchFilter,SearchControls)}.
+   *
+   * @param  filter  <code>SearchFilter</code> expression to use for the search
+   * @param  searchControls  <code>SearchControls</code> to search with
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> search(
+    final SearchFilter filter,
+    final SearchControls searchControls)
+    throws NamingException
+  {
+    return this.search(this.config.getBaseDn(), filter, searchControls);
+  }
+
+
+  /**
+   * This will query the LDAP with the supplied dn and filter. All attributes
+   * will be returned. Search controls will be created from {@link
+   * LdapConfig#getSearchControls(String[])}. See {@link
+   * #search(String,SearchFilter,String[])}.
+   *
+   * @param  dn  <code>String</code> name to begin search at
+   * @param  filter  <code>SearchFilter</code> expression to use for the search
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> search(
+    final String dn,
+    final SearchFilter filter)
+    throws NamingException
+  {
+    return this.search(dn, filter, this.config.getSearchControls(null));
+  }
+
+
+  /**
+   * This will query the LDAP with the supplied dn, filter, and return
+   * attributes. Search controls will be created from {@link
+   * LdapConfig#getSearchControls(String[])}. See {@link
+   * #search(String,SearchFilter,SearchControls,SearchResultHandler[])}.
+   *
+   * @param  dn  <code>String</code> name to begin search at
+   * @param  filter  <code>SearchFilter</code> expression to use for the search
+   * @param  retAttrs  <code>String[]</code> attributes to return
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> search(
+    final String dn,
+    final SearchFilter filter,
+    final String[] retAttrs)
+    throws NamingException
+  {
+    return
+      this.search(
+        dn,
+        filter,
+        this.config.getSearchControls(retAttrs),
+        this.config.getSearchResultHandlers());
+  }
+
+
+  /**
+   * This will query the LDAP with the supplied dn, filter, and search controls.
+   * See {@link
+   * #search(String,SearchFilter,SearchControls,SearchResultHandler[])}.
+   *
+   * @param  dn  <code>String</code> name to begin search at
+   * @param  filter  <code>SearchFilter</code> expression to use for the search
+   * @param  searchControls  <code>SearchControls</code> to search with
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> search(
+    final String dn,
+    final SearchFilter filter,
+    final SearchControls searchControls)
+    throws NamingException
+  {
+    return
+      this.search(
+        dn,
+        filter,
+        searchControls,
+        this.config.getSearchResultHandlers());
+  }
+
+
+  /**
+   * This will query the LDAP with the supplied dn, filter, return attributes,
+   * and search result handler. Search controls will be created from {@link
+   * LdapConfig#getSearchControls(String[])}. See {@link #search(
+   * String,SearchFilter,SearchControls,SearchResultHandler...)}.
+   *
+   * @param  dn  <code>String</code> name to begin search at
+   * @param  filter  <code>SearchFilter</code> expression to use for the search
+   * @param  retAttrs  <code>String[]</code> attributes to return
+   * @param  handler  <code>SearchResultHandler[]</code> of handlers to execute
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> search(
+    final String dn,
+    final SearchFilter filter,
+    final String[] retAttrs,
+    final SearchResultHandler... handler)
+    throws NamingException
+  {
+    return
+      this.search(dn, filter, this.config.getSearchControls(retAttrs), handler);
+  }
+
+
+  /**
+   * This will query the LDAP with the supplied dn, filter, search controls, and
+   * search result handler. If {@link LdapConfig#getPagedResultsSize()} is
+   * greater than 0, the PagedResultsControl will be invoked. See {@link
+   * AbstractLdap
+   * #search(String,String,Object[],SearchControls,SearchResultHandler[])}.
+   *
+   * @param  dn  <code>String</code> name to begin search at
+   * @param  filter  <code>SearchFilter</code> expression to use for the search
+   * @param  searchControls  <code>SearchControls</code> to search with
+   * @param  handler  <code>SearchResultHandler[]</code> of handlers to execute
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> search(
+    final String dn,
+    final SearchFilter filter,
+    final SearchControls searchControls,
+    final SearchResultHandler... handler)
+    throws NamingException
+  {
+    if (handler != null && handler.length > 0) {
+      for (SearchResultHandler h : handler) {
+        if (ExtendedSearchResultHandler.class.isInstance(h)) {
+          ((ExtendedSearchResultHandler) h).setSearchResultLdap(this);
+        }
+
+        final AttributeHandler[] attrHandler = h.getAttributeHandler();
+        if (attrHandler != null && attrHandler.length > 0) {
+          for (AttributeHandler ah : attrHandler) {
+            if (ExtendedAttributeHandler.class.isInstance(ah)) {
+              ((ExtendedAttributeHandler) ah).setSearchResultLdap(this);
+            }
+          }
+        }
+      }
+    }
+    if (this.config.getPagedResultsSize() > 0) {
+      return
+        super.pagedSearch(
+          dn,
+          filter.getFilter(),
+          filter.getFilterArgs().toArray(),
+          searchControls,
+          handler);
+    } else {
+      return
+        super.search(
+          dn,
+          filter.getFilter(),
+          filter.getFilterArgs().toArray(),
+          searchControls,
+          handler);
+    }
+  }
+
+
+  /**
+   * This will query the LDAP for the supplied matching attributes. All
+   * attributes will be returned. {@link LdapConfig#getBaseDn()} is used as the
+   * name to search.
+   * See {@link #searchAttributes(String, Attributes, String[])}.
+   *
+   * @param  matchAttrs  <code>Attributes</code> attributes to match
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> searchAttributes(final Attributes matchAttrs)
+    throws NamingException
+  {
+    return this.searchAttributes(this.config.getBaseDn(), matchAttrs, null);
+  }
+
+
+  /**
+   * This will query the LDAP for the supplied matching attributes and return
+   * attributes. {@link LdapConfig#getBaseDn()} is used as the name to search.
+   * See {@link #searchAttributes(String, Attributes, String[])}.
+   *
+   * @param  matchAttrs  <code>Attributes</code> attributes to match
+   * @param  retAttrs  <code>String[]</code> attributes to return
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> searchAttributes(
+    final Attributes matchAttrs,
+    final String[] retAttrs)
+    throws NamingException
+  {
+    return this.searchAttributes(this.config.getBaseDn(), matchAttrs, retAttrs);
+  }
+
+
+  /**
+   * This will query the LDAP for the supplied dn and matching attributes. All
+   * attributes will be returned. See {@link #searchAttributes(String,
+   * Attributes, String[])}.
+   *
+   * @param  dn  <code>String</code> name to search in
+   * @param  matchAttrs  <code>Attributes</code> attributes to match
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> searchAttributes(
+    final String dn,
+    final Attributes matchAttrs)
+    throws NamingException
+  {
+    return this.searchAttributes(dn, matchAttrs, null);
+  }
+
+
+  /**
+   * This will query the LDAP for the supplied dn, matching attributes and
+   * return attributes. See {@link #searchAttributes( String, Attributes,
+   * String[], SearchResultHandler[])}. This method converts relative DNs to
+   * fully qualified DNs, no post processing is required
+   *
+   * @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
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Iterator<SearchResult> searchAttributes(
+    final String dn,
+    final Attributes matchAttrs,
+    final String[] retAttrs)
+    throws NamingException
+  {
+    return
+      this.searchAttributes(
+        dn,
+        matchAttrs,
+        retAttrs,
+        this.config.getSearchResultHandlers());
+  }
+
+
+  /** {@inheritDoc} */
+  public Iterator<SearchResult> searchAttributes(
+    final String dn,
+    final Attributes matchAttrs,
+    final String[] retAttrs,
+    final SearchResultHandler... handler)
+    throws NamingException
+  {
+    if (handler != null && handler.length > 0) {
+      for (SearchResultHandler h : handler) {
+        if (ExtendedSearchResultHandler.class.isInstance(h)) {
+          ((ExtendedSearchResultHandler) h).setSearchResultLdap(this);
+        }
+
+        final AttributeHandler[] attrHandler = h.getAttributeHandler();
+        if (attrHandler != null && attrHandler.length > 0) {
+          for (AttributeHandler ah : attrHandler) {
+            if (ExtendedAttributeHandler.class.isInstance(ah)) {
+              ((ExtendedAttributeHandler) ah).setSearchResultLdap(this);
+            }
+          }
+        }
+      }
+    }
+    return super.searchAttributes(dn, matchAttrs, retAttrs, handler);
+  }
+
+
+  /** {@inheritDoc} */
+  public Iterator<NameClassPair> list(final String dn)
+    throws NamingException
+  {
+    return super.list(dn);
+  }
+
+
+  /** {@inheritDoc} */
+  public Iterator<Binding> listBindings(final String dn)
+    throws NamingException
+  {
+    return super.listBindings(dn);
+  }
+
+
+  /**
+   * This will return all the attributes associated with the supplied dn. See
+   * {@link #getAttributes(String, String[])}.
+   *
+   * @param  dn  <code>String</code> named object in the LDAP
+   *
+   * @return  <code>Attributes</code>
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Attributes getAttributes(final String dn)
+    throws NamingException
+  {
+    return this.getAttributes(dn, null);
+  }
+
+
+  /**
+   * 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
+   * #getAttributes(String, String[], AttributeHandler[])}.
+   *
+   * @param  dn  <code>String</code> named object in the LDAP
+   * @param  retAttrs  <code>String[]</code> attributes to return
+   *
+   * @return  <code>Attributes</code>
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public Attributes getAttributes(final String dn, final String[] retAttrs)
+    throws NamingException
+  {
+    return this.getAttributes(dn, retAttrs, new AttributeHandler[0]);
+  }
+
+
+  /** {@inheritDoc} */
+  public Attributes getAttributes(
+    final String dn,
+    final String[] retAttrs,
+    final AttributeHandler... handler)
+    throws NamingException
+  {
+    if (handler != null && handler.length > 0) {
+      for (AttributeHandler h : handler) {
+        if (ExtendedAttributeHandler.class.isInstance(h)) {
+          ((ExtendedAttributeHandler) h).setSearchResultLdap(this);
+        }
+      }
+    }
+    return super.getAttributes(dn, retAttrs, handler);
+  }
+
+
+  /** {@inheritDoc} */
+  public Iterator<SearchResult> getSchema(final String dn)
+    throws NamingException
+  {
+    return super.getSchema(dn);
+  }
+
+
+  /**
+   * This will modify the supplied attributes for the supplied value given by
+   * the modification operation. See {@link
+   * AbstractLdap#modifyAttributes(String, int, Attributes)}.
+   *
+   * @param  dn  <code>String</code> named object in the LDAP
+   * @param  mod  <code>AttributeModification</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
+   */
+  public void modifyAttributes(
+    final String dn,
+    final AttributeModification mod,
+    final Attributes attrs)
+    throws NamingException
+  {
+    super.modifyAttributes(dn, mod.modOp(), attrs);
+  }
+
+
+  /** {@inheritDoc} */
+  public void modifyAttributes(final String dn, final ModificationItem[] mods)
+    throws NamingException
+  {
+    super.modifyAttributes(dn, mods);
+  }
+
+
+  /** {@inheritDoc} */
+  public void create(final String dn, final Attributes attrs)
+    throws NamingException
+  {
+    super.create(dn, attrs);
+  }
+
+
+  /** {@inheritDoc} */
+  public void rename(final String oldDn, final String newDn)
+    throws NamingException
+  {
+    super.rename(oldDn, newDn);
+  }
+
+
+  /** {@inheritDoc} */
+  public void delete(final String dn)
+    throws NamingException
+  {
+    super.delete(dn);
+  }
+
+
+  /**
+   * This will return a list of SASL mechanisms that this LDAP supports.
+   *
+   * @return  <code>String[]</code> - supported SASL mechanisms
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public String[] getSaslMechanisms()
+    throws NamingException
+  {
+    final Attributes attrs = this.getAttributes(
+      "",
+      new String[] {LdapConstants.SUPPORTED_SASL_MECHANISMS});
+
+    String[] results = new String[0];
+    if (attrs != null) {
+      final Attribute attr = attrs.get(LdapConstants.SUPPORTED_SASL_MECHANISMS);
+      if (attr != null) {
+        results = (String[]) COPY_RESULT_HANDLER.process(
+          new SearchCriteria(""),
+          attr.getAll()).toArray(results);
+      }
+    }
+
+    return results;
+  }
+
+
+  /**
+   * This will return a list of controls that this LDAP supports.
+   *
+   * @return  <code>String[]</code> - supported controls
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public String[] getSupportedControls()
+    throws NamingException
+  {
+    final Attributes attrs = this.getAttributes(
+      "",
+      new String[] {LdapConstants.SUPPORTED_CONTROL});
+
+    String[] results = new String[0];
+    if (attrs != null) {
+      final Attribute attr = attrs.get(LdapConstants.SUPPORTED_CONTROL);
+      if (attr != null) {
+        results = (String[]) COPY_RESULT_HANDLER.process(
+          new SearchCriteria(""),
+          attr.getAll()).toArray(results);
+      }
+    }
+
+    return results;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/LdapCli.java b/src/main/java/edu/vt/middleware/ldap/LdapCli.java
new file mode 100644
index 0000000..0918779
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/LdapCli.java
@@ -0,0 +1,179 @@
+/*
+  $Id: LdapCli.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.Iterator;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.dsml.Dsmlv1;
+import edu.vt.middleware.ldap.dsml.Dsmlv2;
+import edu.vt.middleware.ldap.ldif.Ldif;
+import edu.vt.middleware.ldap.props.LdapConfigPropertyInvoker;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+
+/**
+ * Command line interface for ldap operations.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdapCli extends AbstractCli
+{
+
+  /** Option for ldap query. */
+  protected static final String OPT_QUERY = "query";
+
+  /** Name of operation provided by this class. */
+  private static final String COMMAND_NAME = "ldapsearch";
+
+
+  /** Default constructor. */
+  public LdapCli()
+  {
+    super();
+    this.opts.add(OPT_QUERY);
+  }
+
+
+  /**
+   * CLI entry point method.
+   *
+   * @param  args  Command line arguments.
+   */
+  public static void main(final String[] args)
+  {
+    new LdapCli().performAction(args);
+  }
+
+
+  /** {@inheritDoc} */
+  protected void initOptions()
+  {
+    super.initOptions(
+      new LdapConfigPropertyInvoker(
+        LdapConfig.class,
+        LdapConfig.PROPERTIES_DOMAIN));
+
+    options.addOption(new Option(OPT_QUERY, true, ""));
+  }
+
+
+  /**
+   * Initialize an LdapConfig with command line options.
+   *
+   * @param  line  Parsed command line arguments container.
+   *
+   * @return  <code>LdapConfig</code> that has been initialized
+   *
+   * @throws  Exception  On errors thrown by handler.
+   */
+  protected LdapConfig initLdapConfig(final CommandLine line)
+    throws Exception
+  {
+    final LdapConfig config = new LdapConfig();
+    this.initLdapProperties(config, line);
+    if (line.hasOption(OPT_TRACE)) {
+      config.setTracePackets(System.out);
+    }
+    if (config.getBindDn() != null && config.getBindCredential() == null) {
+      // prompt the user to enter a password
+      System.out.print(
+        "Enter password for service user " + config.getBindDn() + ": ");
+
+      final String pass = (new BufferedReader(new InputStreamReader(System.in)))
+          .readLine();
+      config.setBindCredential(pass);
+    }
+    return config;
+  }
+
+
+  /** {@inheritDoc} */
+  protected void dispatch(final CommandLine line)
+    throws Exception
+  {
+    if (line.hasOption(OPT_DSMLV1)) {
+      this.outputDsmlv1 = true;
+    } else if (line.hasOption(OPT_DSMLV2)) {
+      this.outputDsmlv2 = true;
+    }
+    if (line.hasOption(OPT_HELP)) {
+      printHelp();
+    } else if (line.hasOption(OPT_QUERY)) {
+      search(
+        initLdapConfig(line),
+        line.getOptionValue(OPT_QUERY),
+        line.getArgs());
+    } else {
+      printExamples();
+    }
+  }
+
+
+  /**
+   * Executes the ldap search operation.
+   *
+   * @param  config  Ldap configuration.
+   * @param  filter  Ldap filter to search on.
+   * @param  attrs  Ldap attributes to return.
+   *
+   * @throws  Exception  On errors.
+   */
+  protected void search(
+    final LdapConfig config,
+    final String filter,
+    final String[] attrs)
+    throws Exception
+  {
+    final Ldap ldap = new Ldap();
+    ldap.setLdapConfig(config);
+
+    try {
+      Iterator<SearchResult> results = null;
+      if (attrs == null || attrs.length == 0) {
+        results = ldap.search(new SearchFilter(filter));
+      } else {
+        results = ldap.search(new SearchFilter(filter), attrs);
+      }
+      if (this.outputDsmlv1) {
+        (new Dsmlv1()).outputDsml(
+          results,
+          new BufferedWriter(new OutputStreamWriter(System.out)));
+      } else if (this.outputDsmlv2) {
+        (new Dsmlv2()).outputDsml(
+          results,
+          new BufferedWriter(new OutputStreamWriter(System.out)));
+      } else {
+        (new Ldif()).outputLdif(
+          results,
+          new BufferedWriter(new OutputStreamWriter(System.out)));
+      }
+    } finally {
+      if (ldap != null) {
+        ldap.close();
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  protected String getCommandName()
+  {
+    return COMMAND_NAME;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/LdapConfig.java b/src/main/java/edu/vt/middleware/ldap/LdapConfig.java
new file mode 100644
index 0000000..34a3608
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/LdapConfig.java
@@ -0,0 +1,1921 @@
+/*
+  $Id: LdapConfig.java 1786 2011-01-05 14:45:07Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1786 $
+  Updated: $Date: 2011-01-05 14:45:07 +0000 (Wed, 05 Jan 2011) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import javax.naming.CommunicationException;
+import javax.naming.LimitExceededException;
+import javax.naming.ServiceUnavailableException;
+import javax.naming.directory.SearchControls;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+import edu.vt.middleware.ldap.handler.DefaultConnectionHandler;
+import edu.vt.middleware.ldap.handler.FqdnSearchResultHandler;
+import edu.vt.middleware.ldap.handler.SearchResultHandler;
+import edu.vt.middleware.ldap.handler.TlsConnectionHandler;
+import edu.vt.middleware.ldap.props.AbstractPropertyConfig;
+import edu.vt.middleware.ldap.props.LdapConfigPropertyInvoker;
+import edu.vt.middleware.ldap.props.LdapProperties;
+
+/**
+ * <code>LdapConfig</code> contains all the configuration data that the <code>
+ * Ldap</code> needs to control connections and searching.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1786 $ $Date: 2011-01-05 14:45:07 +0000 (Wed, 05 Jan 2011) $
+ */
+public class LdapConfig extends AbstractPropertyConfig
+{
+
+
+  /** Domain to look for ldap properties in, value is {@value}. */
+  public static final String PROPERTIES_DOMAIN = "edu.vt.middleware.ldap.";
+
+  /** Invoker for ldap properties. */
+  private static final LdapConfigPropertyInvoker PROPERTIES =
+    new LdapConfigPropertyInvoker(LdapConfig.class, PROPERTIES_DOMAIN);
+
+
+  /**
+   * Enum to define the type of search scope. See {@link
+   * javax.naming.directory.SearchControls}.
+   */
+  public enum SearchScope {
+
+    /** object level search. */
+    OBJECT(SearchControls.OBJECT_SCOPE),
+
+    /** one level search. */
+    ONELEVEL(SearchControls.ONELEVEL_SCOPE),
+
+    /** subtree search. */
+    SUBTREE(SearchControls.SUBTREE_SCOPE);
+
+    /** underlying search scope integer. */
+    private int scope;
+
+
+    /**
+     * Creates a new <code>SearchScope</code> with the supplied integer.
+     *
+     * @param  i  search scope
+     */
+    SearchScope(final int i)
+    {
+      this.scope = i;
+    }
+
+
+    /**
+     * Returns the search scope integer.
+     *
+     * @return  <code>int</code>
+     */
+    public int scope()
+    {
+      return this.scope;
+    }
+
+
+    /**
+     * Method to convert a JNDI constant value to an enum. Returns null if the
+     * supplied constant does not match a known value.
+     *
+     * @param  i  search scope
+     *
+     * @return  search scope
+     */
+    public static SearchScope parseSearchScope(final int i)
+    {
+      SearchScope ss = null;
+      if (OBJECT.scope() == i) {
+        ss = OBJECT;
+      } else if (ONELEVEL.scope() == i) {
+        ss = ONELEVEL;
+      } else if (SUBTREE.scope() == i) {
+        ss = SUBTREE;
+      }
+      return ss;
+    }
+  }
+
+  /** Default context factory. */
+  private String contextFactory = LdapConstants.DEFAULT_CONTEXT_FACTORY;
+
+  /** Default connection handler. */
+  private ConnectionHandler connectionHandler = new DefaultConnectionHandler(
+    this);
+
+  /** Default ldap socket factory used for SSL and TLS. */
+  private SSLSocketFactory sslSocketFactory;
+
+  /** Default hostname verifier for TLS connections. */
+  private HostnameVerifier hostnameVerifier;
+
+  /** URL to the LDAP(s). */
+  private String ldapUrl;
+
+  /** Hostname of the LDAP server. */
+  private String host;
+
+  /** Port the LDAP server is listening on. */
+  private String port = LdapConstants.DEFAULT_PORT;
+
+  /** Amount of time in milliseconds that connect operations will block. */
+  private Integer timeout;
+
+  /** DN to bind as before performing operations. */
+  private String bindDn;
+
+  /** Credential for the bind DN. */
+  private Object bindCredential;
+
+  /** Base dn for LDAP searching. */
+  private String baseDn = LdapConstants.DEFAULT_BASE_DN;
+
+  /** Type of search scope to use, default is subtree. */
+  private SearchScope searchScope = SearchScope.SUBTREE;
+
+  /** Security level to use when binding to the LDAP. */
+  private String authtype = LdapConstants.DEFAULT_AUTHTYPE;
+
+  /** Whether to require the most authoritative source for this service. */
+  private boolean authoritative = LdapConstants.DEFAULT_AUTHORITATIVE;
+
+  /** Preferred batch size to use when returning results. */
+  private Integer batchSize;
+
+  /** Amount of time in milliseconds that search operations will block. */
+  private Integer timeLimit;
+
+  /** Maximum number of entries that search operations will return. */
+  private Long countLimit;
+
+  /** Size of result set when using paged searching. */
+  private Integer pagedResultsSize;
+
+  /** Number of times to retry ldap operations on communication exception. */
+  private Integer operationRetry;
+
+  /** Exception types to retry operations on. */
+  private Class<?>[] operationRetryExceptions = new Class[] {
+    CommunicationException.class,
+    ServiceUnavailableException.class,
+  };
+
+  /** Amount of time in milliseconds to wait before retrying. */
+  private Long operationRetryWait;
+
+  /** Factor to multiply operation retry wait by. */
+  private Integer operationRetryBackoff;
+
+  /** Whether link dereferencing should be performed during the search. */
+  private boolean derefLinkFlag;
+
+  /** Whether objects will be returned in the result. */
+  private boolean returningObjFlag;
+
+  /** DNS host to use for JNDI URL context implementation. */
+  private String dnsUrl;
+
+  /** Preferred language as defined by RFC 1766. */
+  private String language;
+
+  /** How the provider should handle referrals. */
+  private String referral;
+
+  /** How the provider should handle aliases. */
+  private String derefAliases;
+
+  /** Additional attributes that should be considered binary. */
+  private String binaryAttributes;
+
+  /** Handlers to process search results. */
+  private SearchResultHandler[] searchResultHandlers =
+    new SearchResultHandler[] {new FqdnSearchResultHandler()};
+
+  /** Exception types to ignore when handling results. */
+  private Class<?>[] handlerIgnoreExceptions = new Class[] {
+    LimitExceededException.class,
+  };
+
+  /** SASL authorization ID. */
+  private String saslAuthorizationId;
+
+  /** SASL realm. */
+  private String saslRealm;
+
+  /** Whether only attribute type names should be returned. */
+  private boolean typesOnly = LdapConstants.DEFAULT_TYPES_ONLY;
+
+  /** Additional environment properties. */
+  private Map<String, Object> additionalEnvironmentProperties =
+    new HashMap<String, Object>();
+
+  /** Whether to log authentication credentials. */
+  private boolean logCredentials = LdapConstants.DEFAULT_LOG_CREDENTIALS;
+
+  /** Connect to LDAP using SSL protocol. */
+  private boolean ssl = LdapConstants.DEFAULT_USE_SSL;
+
+  /** Stream to print LDAP ASN.1 BER packets. */
+  private PrintStream tracePackets;
+
+
+  /** Default constructor. */
+  public LdapConfig() {}
+
+
+  /**
+   * This will create a new <code>LdapConfig</code> with the supplied ldap url.
+   *
+   * @param  ldapUrl  <code>String</code> LDAP URL
+   */
+  public LdapConfig(final String ldapUrl)
+  {
+    this();
+    this.setLdapUrl(ldapUrl);
+  }
+
+
+  /**
+   * This will create a new <code>LdapConfig</code> with the supplied ldap url
+   * and base Strings.
+   *
+   * @param  ldapUrl  <code>String</code> LDAP URL
+   * @param  baseDn  <code>String</code> LDAP base DN
+   */
+  public LdapConfig(final String ldapUrl, final String baseDn)
+  {
+    this();
+    this.setLdapUrl(ldapUrl);
+    this.setBaseDn(baseDn);
+  }
+
+
+  /**
+   * This returns the Context environment properties that are used to make LDAP
+   * connections.
+   *
+   * @return  <code>Hashtable</code> - context environment
+   */
+  public Hashtable<String, ?> getEnvironment()
+  {
+    final Hashtable<String, Object> environment =
+      new Hashtable<String, Object>();
+    environment.put(LdapConstants.CONTEXT_FACTORY, this.contextFactory);
+
+    if (this.authoritative) {
+      environment.put(
+        LdapConstants.AUTHORITATIVE,
+        Boolean.valueOf(this.authoritative).toString());
+    }
+
+    if (this.batchSize != null) {
+      environment.put(LdapConstants.BATCH_SIZE, this.batchSize.toString());
+    }
+
+    if (this.dnsUrl != null) {
+      environment.put(LdapConstants.DNS_URL, this.dnsUrl);
+    }
+
+    if (this.language != null) {
+      environment.put(LdapConstants.LANGUAGE, this.language);
+    }
+
+    if (this.referral != null) {
+      environment.put(LdapConstants.REFERRAL, this.referral);
+    }
+
+    if (this.derefAliases != null) {
+      environment.put(LdapConstants.DEREF_ALIASES, this.derefAliases);
+    }
+
+    if (this.binaryAttributes != null) {
+      environment.put(LdapConstants.BINARY_ATTRIBUTES, this.binaryAttributes);
+    }
+
+    if (this.saslAuthorizationId != null) {
+      environment.put(
+        LdapConstants.SASL_AUTHORIZATION_ID,
+        this.saslAuthorizationId);
+    }
+
+    if (this.saslRealm != null) {
+      environment.put(LdapConstants.SASL_REALM, this.saslRealm);
+    }
+
+    if (this.typesOnly) {
+      environment.put(
+        LdapConstants.TYPES_ONLY,
+        Boolean.valueOf(this.typesOnly).toString());
+    }
+
+    if (this.ssl) {
+      environment.put(LdapConstants.PROTOCOL, LdapConstants.SSL_PROTOCOL);
+      if (this.sslSocketFactory != null) {
+        environment.put(
+          LdapConstants.SOCKET_FACTORY,
+          this.sslSocketFactory.getClass().getName());
+      }
+    }
+
+    if (this.tracePackets != null) {
+      environment.put(LdapConstants.TRACE, this.tracePackets);
+    }
+
+    if (this.ldapUrl != null) {
+      environment.put(LdapConstants.PROVIDER_URL, this.ldapUrl);
+    }
+
+    if (this.timeout != null) {
+      environment.put(LdapConstants.TIMEOUT, this.timeout.toString());
+    }
+
+    if (!this.additionalEnvironmentProperties.isEmpty()) {
+      for (
+        Map.Entry<String, Object> entry :
+          this.additionalEnvironmentProperties.entrySet()) {
+        environment.put(entry.getKey(), entry.getValue());
+      }
+    }
+
+    return environment;
+  }
+
+
+  /**
+   * This returns the context factory of the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - context factory
+   */
+  public String getContextFactory()
+  {
+    return this.contextFactory;
+  }
+
+
+  /**
+   * This returns the connection handler of the <code>LdapConfig</code>.
+   *
+   * @return  <code>ConnectionHandler</code> - connection handler
+   */
+  public ConnectionHandler getConnectionHandler()
+  {
+    return this.connectionHandler;
+  }
+
+
+  /**
+   * This returns the SSL socket factory of the <code>LdapConfig</code>.
+   *
+   * @return  <code>SSLSocketFactory</code> - SSL socket factory
+   */
+  public SSLSocketFactory getSslSocketFactory()
+  {
+    return this.sslSocketFactory;
+  }
+
+
+  /**
+   * This returns whether the <code>LdapConfig</code> is using a custom SSL
+   * socket factory.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean useSslSocketFactory()
+  {
+    return this.sslSocketFactory != null;
+  }
+
+
+  /**
+   * This returns the hostname verifier of the <code>LdapConfig</code>.
+   *
+   * @return  <code>HostnameVerifier</code> - hostname verifier
+   */
+  public HostnameVerifier getHostnameVerifier()
+  {
+    return this.hostnameVerifier;
+  }
+
+
+  /**
+   * This returns whether the <code>LdapConfig</code> is using a custom hostname
+   * verifier.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean useHostnameVerifier()
+  {
+    return this.hostnameVerifier != null;
+  }
+
+
+  /**
+   * This returns the ldap url of the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - ldap url
+   */
+  public String getLdapUrl()
+  {
+    return this.ldapUrl;
+  }
+
+
+  /**
+   * This returns the hostname of the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - hostname
+   *
+   * @deprecated  use {@link #getLdapUrl()} instead
+   */
+  @Deprecated
+  public String getHost()
+  {
+    return this.host;
+  }
+
+
+  /**
+   * This returns the port of the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - port
+   *
+   * @deprecated  use {@link #getLdapUrl()} instead
+   */
+  @Deprecated
+  public String getPort()
+  {
+    return this.port;
+  }
+
+
+  /**
+   * This returns the timeout for the <code>LdapConfig</code>. If this value is
+   * 0, then connect operations will wait indefinitely.
+   *
+   * @return  <code>int</code> - timeout
+   */
+  public int getTimeout()
+  {
+    int time = LdapConstants.DEFAULT_TIMEOUT;
+    if (this.timeout != null) {
+      time = this.timeout.intValue();
+    }
+    return time;
+  }
+
+
+  /**
+   * This returns the bind DN.
+   *
+   * @return  <code>String</code> - DN to bind as
+   */
+  public String getBindDn()
+  {
+    return this.bindDn;
+  }
+
+
+  /**
+   * This returns the username of the service user.
+   *
+   * @return  <code>String</code> - username
+   *
+   * @deprecated  use {@link #getBindDn()} instead
+   */
+  @Deprecated
+  public String getServiceUser()
+  {
+    return this.getBindDn();
+  }
+
+
+  /**
+   * This returns the credential used with the bind DN.
+   *
+   * @return  <code>Object</code> - bind DN credential
+   */
+  public Object getBindCredential()
+  {
+    return this.bindCredential;
+  }
+
+
+  /**
+   * This returns the credential of the service user.
+   *
+   * @return  <code>Object</code> - credential
+   *
+   * @deprecated  use {@link #getBindCredential()} instead
+   */
+  @Deprecated
+  public Object getServiceCredential()
+  {
+    return this.getBindCredential();
+  }
+
+
+  /**
+   * This returns the base dn for the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - base dn
+   *
+   * @deprecated  use {@link #getBaseDn()} instead
+   */
+  public String getBase()
+  {
+    return this.getBaseDn();
+  }
+
+
+  /**
+   * This returns the base dn for the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - base dn
+   */
+  public String getBaseDn()
+  {
+    return this.baseDn;
+  }
+
+
+  /**
+   * This returns the search scope for the <code>LdapConfig</code>.
+   *
+   * @return  <code>SearchScope</code> - search scope
+   */
+  public SearchScope getSearchScope()
+  {
+    return this.searchScope;
+  }
+
+
+  /**
+   * This returns whether the search scope is set to object.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isObjectSearchScope()
+  {
+    return this.searchScope == SearchScope.OBJECT;
+  }
+
+
+  /**
+   * This returns whether the search scope is set to one level.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isOneLevelSearchScope()
+  {
+    return this.searchScope == SearchScope.ONELEVEL;
+  }
+
+
+  /**
+   * This returns whether the search scope is set to sub tree.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isSubTreeSearchScope()
+  {
+    return this.searchScope == SearchScope.SUBTREE;
+  }
+
+
+  /**
+   * This returns the security level for the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - security level
+   */
+  public String getAuthtype()
+  {
+    return this.authtype;
+  }
+
+
+  /**
+   * This returns whether the security authentication context is set to 'none'.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isAnonymousAuth()
+  {
+    return this.authtype.equalsIgnoreCase(LdapConstants.NONE_AUTHTYPE);
+  }
+
+
+  /**
+   * This returns whether the security authentication context is set to
+   * 'simple'.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isSimpleAuth()
+  {
+    return this.authtype.equalsIgnoreCase(LdapConstants.SIMPLE_AUTHTYPE);
+  }
+
+
+  /**
+   * This returns whether the security authentication context is set to
+   * 'strong'.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isStrongAuth()
+  {
+    return this.authtype.equalsIgnoreCase(LdapConstants.STRONG_AUTHTYPE);
+  }
+
+
+  /**
+   * This returns whether the security authentication context will perform a
+   * SASL bind as defined by the supported SASL mechanisms.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isSaslAuth()
+  {
+    boolean authtypeSasl = false;
+    for (String sasl : LdapConstants.SASL_MECHANISMS) {
+      if (this.authtype.equalsIgnoreCase(sasl)) {
+        authtypeSasl = true;
+        break;
+      }
+    }
+    return authtypeSasl;
+  }
+
+
+  /**
+   * This returns whether the security authentication context is set to
+   * 'EXTERNAL'.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isExternalAuth()
+  {
+    return
+      this.authtype.equalsIgnoreCase(LdapConstants.SASL_MECHANISM_EXTERNAL);
+  }
+
+
+  /**
+   * This returns whether the security authentication context is set to
+   * 'DIGEST-MD5'.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isDigestMD5Auth()
+  {
+    return
+      this.authtype.equalsIgnoreCase(LdapConstants.SASL_MECHANISM_DIGEST_MD5);
+  }
+
+
+  /**
+   * This returns whether the security authentication context is set to
+   * 'CRAM-MD5'.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isCramMD5Auth()
+  {
+    return
+      this.authtype.equalsIgnoreCase(LdapConstants.SASL_MECHANISM_CRAM_MD5);
+  }
+
+
+  /**
+   * This returns whether the security authentication context is set to
+   * 'GSSAPI'.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isGSSAPIAuth()
+  {
+    return this.authtype.equalsIgnoreCase(LdapConstants.SASL_MECHANISM_GSS_API);
+  }
+
+
+  /**
+   * See {@link #isAuthoritative()}.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean getAuthoritative()
+  {
+    return this.isAuthoritative();
+  }
+
+
+  /**
+   * This returns whether the <code>LdapConfig</code> is set to require a
+   * authoritative source.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isAuthoritative()
+  {
+    return this.authoritative;
+  }
+
+
+  /**
+   * This returns the time limit for the <code>LdapConfig</code>. If this value
+   * is 0, then search operations will wait indefinitely for an answer.
+   *
+   * @return  <code>int</code> - time limit
+   */
+  public int getTimeLimit()
+  {
+    int limit = LdapConstants.DEFAULT_TIME_LIMIT;
+    if (this.timeLimit != null) {
+      limit = this.timeLimit.intValue();
+    }
+    return limit;
+  }
+
+
+  /**
+   * This returns the count limit for the <code>LdapConfig</code>. If this value
+   * is 0, then search operations will return all the results it finds.
+   *
+   * @return  <code>long</code> - count limit
+   */
+  public long getCountLimit()
+  {
+    long limit = LdapConstants.DEFAULT_COUNT_LIMIT;
+    if (this.countLimit != null) {
+      limit = this.countLimit.longValue();
+    }
+    return limit;
+  }
+
+
+  /**
+   * This returns the paged results size for the <code>LdapConfig</code>. This
+   * value is used whenever the PagedResultsControl in invoked.
+   *
+   * @return  <code>int</code> - page size
+   */
+  public int getPagedResultsSize()
+  {
+    int size = LdapConstants.DEFAULT_PAGED_RESULTS_SIZE;
+    if (this.pagedResultsSize != null) {
+      size = this.pagedResultsSize.intValue();
+    }
+    return size;
+  }
+
+
+  /**
+   * This returns the number of times ldap operations will be retried if a
+   * communication exception occurs. If this value is 0, no retries will occur.
+   *
+   * @return  <code>int</code> - retry count
+   */
+  public int getOperationRetry()
+  {
+    int retry = LdapConstants.DEFAULT_OPERATION_RETRY;
+    if (this.operationRetry != null) {
+      retry = this.operationRetry.intValue();
+    }
+    return retry;
+  }
+
+
+  /**
+   * This returns the exception types to retry operations on.
+   *
+   * @return  <code>Class[]</code>
+   */
+  public Class<?>[] getOperationRetryExceptions()
+  {
+    return this.operationRetryExceptions;
+  }
+
+
+  /**
+   * This returns the operation retry wait time for the <code>LdapConfig</code>.
+   *
+   * @return  <code>int</code> - time limit
+   */
+  public long getOperationRetryWait()
+  {
+    long wait = LdapConstants.DEFAULT_OPERATION_RETRY_WAIT;
+    if (this.operationRetryWait != null) {
+      wait = this.operationRetryWait.intValue();
+    }
+    return wait;
+  }
+
+
+  /**
+   * This returns the factor by which to multiply the operation retry wait time.
+   * This allows clients to progressively delay each retry. The formula for
+   * backoff is (wait * backoff * attempt). So a wait time of 2s with a backoff
+   * of 3 will delay by 6s, then 12s, then 18s, and so forth.
+   *
+   * @return  <code>int</code> - backoff factor
+   */
+  public int getOperationRetryBackoff()
+  {
+    int backoff = LdapConstants.DEFAULT_OPERATION_RETRY_BACKOFF;
+    if (this.operationRetryBackoff != null) {
+      backoff = this.operationRetryBackoff.intValue();
+    }
+    return backoff;
+  }
+
+
+  /**
+   * This returns the derefLinkFlag for the <code>LdapConfig</code>.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean getDerefLinkFlag()
+  {
+    return this.derefLinkFlag;
+  }
+
+
+  /**
+   * This returns the returningObjFlag for the <code>LdapConfig</code>.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean getReturningObjFlag()
+  {
+    return this.returningObjFlag;
+  }
+
+
+  /**
+   * This returns the batch size for the <code>LdapConfig</code>. If this value
+   * is -1, then the default provider setting is being used.
+   *
+   * @return  <code>int</code> - batch size
+   */
+  public int getBatchSize()
+  {
+    int size = LdapConstants.DEFAULT_BATCH_SIZE;
+    if (this.batchSize != null) {
+      size = this.batchSize.intValue();
+    }
+    return size;
+  }
+
+
+  /**
+   * This returns the dns url for the <code>LdapConfig</code>. If this value is
+   * null, then this property is not being used.
+   *
+   * @return  <code>String</code> - dns url
+   */
+  public String getDnsUrl()
+  {
+    return this.dnsUrl;
+  }
+
+
+  /**
+   * This returns the preferred language for the <code>LdapConfig</code>. If
+   * this value is null, then the default provider setting is being used.
+   *
+   * @return  <code>String</code> - language
+   */
+  public String getLanguage()
+  {
+    return this.language;
+  }
+
+
+  /**
+   * This returns the referral setting for the <code>LdapConfig</code>. If this
+   * value is null, then the default provider setting is being used.
+   *
+   * @return  <code>String</code> - referral
+   */
+  public String getReferral()
+  {
+    return this.referral;
+  }
+
+
+  /**
+   * This returns the alias setting for the <code>LdapConfig</code>. If this
+   * value is null, then the default provider setting is being used.
+   *
+   * @return  <code>String</code> - alias
+   */
+  public String getDerefAliases()
+  {
+    return this.derefAliases;
+  }
+
+
+  /**
+   * This returns additional binary attributes for the <code>LdapConfig</code>.
+   * If this value is null, then the default provider setting is being used.
+   *
+   * @return  <code>String</code> - binary attributes
+   */
+  public String getBinaryAttributes()
+  {
+    return this.binaryAttributes;
+  }
+
+
+  /**
+   * This returns the handlers to use for processing search results.
+   *
+   * @return  <code>SearchResultHandler[]</code>
+   */
+  public SearchResultHandler[] getSearchResultHandlers()
+  {
+    return this.searchResultHandlers;
+  }
+
+
+  /**
+   * This returns the exception types to ignore when handling results.
+   *
+   * @return  <code>Class[]</code>
+   */
+  public Class<?>[] getHandlerIgnoreExceptions()
+  {
+    return this.handlerIgnoreExceptions;
+  }
+
+
+  /**
+   * This returns ths SASL authorization id for the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - authorization id
+   */
+  public String getSaslAuthorizationId()
+  {
+    return this.saslAuthorizationId;
+  }
+
+
+  /**
+   * This returns ths SASL realm for the <code>LdapConfig</code>.
+   *
+   * @return  <code>String</code> - realm
+   */
+  public String getSaslRealm()
+  {
+    return this.saslRealm;
+  }
+
+
+  /**
+   * See {@link #isTypesOnly()}.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean getTypesOnly()
+  {
+    return this.isTypesOnly();
+  }
+
+
+  /**
+   * This returns whether the <code>LdapConfig</code> is set to only return
+   * attribute types.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isTypesOnly()
+  {
+    return this.typesOnly;
+  }
+
+
+  /**
+   * This returns any environment properties that may have been set for the
+   * <code>LdapConfig</code> using {@link
+   * #setEnvironmentProperties(String,String)} that do not represent properties
+   * of this config. The collection returned is unmodifiable.
+   *
+   * @return  <code>Map</code> - additional environment properties
+   */
+  public Map<String, Object> getEnvironmentProperties()
+  {
+    return Collections.unmodifiableMap(this.additionalEnvironmentProperties);
+  }
+
+
+  /**
+   * This returns whether authentication credentials will be logged.
+   *
+   * @return  <code>boolean</code> - whether authentication credentials will be
+   * logged.
+   */
+  public boolean getLogCredentials()
+  {
+    return this.logCredentials;
+  }
+
+
+  /**
+   * See {@link #isSslEnabled()}.
+   *
+   * @return  <code>boolean</code> - whether the SSL protocol is being used
+   */
+  public boolean getSsl()
+  {
+    return this.isSslEnabled();
+  }
+
+
+  /**
+   * This returns whether the <code>LdapConfig</code> is using the SSL protocol
+   * for connections.
+   *
+   * @return  <code>boolean</code> - whether the SSL protocol is being used
+   */
+  public boolean isSslEnabled()
+  {
+    return this.ssl;
+  }
+
+
+  /**
+   * See {@link #isTlsEnabled()}.
+   *
+   * @return  <code>boolean</code> - whether the TLS protocol is being used
+   */
+  public boolean getTls()
+  {
+    return this.isTlsEnabled();
+  }
+
+
+  /**
+   * This returns whether the <code>LdapConfig</code> is using the TLS protocol
+   * for connections.
+   *
+   * @return  <code>boolean</code> - whether the TLS protocol is being used
+   */
+  public boolean isTlsEnabled()
+  {
+    return
+      this.connectionHandler != null &&
+        this.connectionHandler.getClass().isAssignableFrom(
+          TlsConnectionHandler.class);
+  }
+
+
+  /**
+   * This sets the context factory of the <code>LdapConfig</code>.
+   *
+   * @param  contextFactory  <code>String</code> context factory
+   */
+  public void setContextFactory(final String contextFactory)
+  {
+    checkImmutable();
+    checkStringInput(contextFactory, false);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting contextFactory: " + contextFactory);
+    }
+    this.contextFactory = contextFactory;
+  }
+
+
+  /**
+   * This sets the connection handler of the <code>LdapConfig</code>.
+   *
+   * @param  connectionHandler  <code>ConnectionHandler</code> connection
+   * handler
+   */
+  public void setConnectionHandler(final ConnectionHandler connectionHandler)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting connectionHandler: " + connectionHandler);
+    }
+    this.connectionHandler = connectionHandler;
+    if (this.connectionHandler != null) {
+      this.connectionHandler.setLdapConfig(this);
+    }
+  }
+
+
+  /**
+   * This sets the SSL socket factory of the <code>LdapConfig</code>.
+   *
+   * @param  sslSocketFactory  <code>SSLSocketFactory</code> SSL socket factory
+   */
+  public void setSslSocketFactory(final SSLSocketFactory sslSocketFactory)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting sslSocketFactory: " + sslSocketFactory);
+    }
+    this.sslSocketFactory = sslSocketFactory;
+  }
+
+
+  /**
+   * This sets the hostname verifier of the <code>LdapConfig</code>.
+   *
+   * @param  hostnameVerifier  <code>HostnameVerifier</code> hostname verifier
+   */
+  public void setHostnameVerifier(final HostnameVerifier hostnameVerifier)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting hostnameVerifier: " + hostnameVerifier);
+    }
+    this.hostnameVerifier = hostnameVerifier;
+  }
+
+
+  /**
+   * This sets the ldap url of the <code>LdapConfig</code>.
+   *
+   * @param  ldapUrl  <code>String</code> url
+   */
+  public void setLdapUrl(final String ldapUrl)
+  {
+    checkImmutable();
+    checkStringInput(ldapUrl, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting ldapUrl: " + ldapUrl);
+    }
+    this.ldapUrl = ldapUrl;
+  }
+
+
+  /**
+   * This sets the hostname of the <code>LdapConfig</code>. The host string may
+   * be of the form ldap://host.domain.name:389, host.domain.name:389, or
+   * host.domain.name. Do not use with {@link #setLdapUrl(String)}.
+   *
+   * @param  host  <code>String</code> hostname
+   *
+   * @deprecated  use {@link #setLdapUrl(String)} instead
+   */
+  @Deprecated
+  public void setHost(final String host)
+  {
+    checkImmutable();
+    if (host != null) {
+      final int prefixLength = LdapConstants.PROVIDER_URL_PREFIX.length();
+      final int separatorLength = LdapConstants.PROVIDER_URL_SEPARATOR.length();
+      String h = host;
+
+      // if host contains '://' and there is data after it, remove the scheme
+      if (
+        h.indexOf(LdapConstants.PROVIDER_URL_PREFIX) != -1 &&
+          h.indexOf(LdapConstants.PROVIDER_URL_PREFIX) + prefixLength <
+          h.length()) {
+        final String scheme = h.substring(
+          0,
+          h.indexOf(LdapConstants.PROVIDER_URL_PREFIX));
+        if (scheme.equalsIgnoreCase(LdapConstants.PROVIDER_URL_SSL_SCHEME)) {
+          this.setSsl(true);
+          this.setPort(LdapConstants.DEFAULT_SSL_PORT);
+        }
+        h = h.substring(
+          h.indexOf(LdapConstants.PROVIDER_URL_PREFIX) + prefixLength,
+          h.length());
+      }
+
+      // if host contains ':' and there is data after it, remove the port
+      if (
+        h.indexOf(LdapConstants.PROVIDER_URL_SEPARATOR) != -1 &&
+          h.indexOf(LdapConstants.PROVIDER_URL_SEPARATOR) + separatorLength <
+          h.length()) {
+        final String p = h.substring(
+          h.indexOf(LdapConstants.PROVIDER_URL_SEPARATOR) + separatorLength,
+          h.length());
+        this.port = p;
+        h = h.substring(0, h.indexOf(LdapConstants.PROVIDER_URL_SEPARATOR));
+      }
+
+      this.host = h;
+      this.setLdapUrl(
+        LdapConstants.PROVIDER_URL_SCHEME + LdapConstants.PROVIDER_URL_PREFIX +
+        this.host + LdapConstants.PROVIDER_URL_SEPARATOR + this.port);
+    }
+  }
+
+
+  /**
+   * This sets the port of the <code>LdapConfig</code>. Do not use with {@link
+   * #setLdapUrl(String)}.
+   *
+   * @param  port  <code>String</code> port
+   *
+   * @deprecated  use {@link #setLdapUrl(String)} instead
+   */
+  @Deprecated
+  public void setPort(final String port)
+  {
+    checkImmutable();
+    this.port = port;
+    if (this.host != null) {
+      this.setLdapUrl(
+        LdapConstants.PROVIDER_URL_SCHEME + LdapConstants.PROVIDER_URL_PREFIX +
+        this.host + LdapConstants.PROVIDER_URL_SEPARATOR + this.port);
+    }
+  }
+
+
+  /**
+   * This sets the maximum amount of time in milliseconds that connect
+   * operations will block.
+   *
+   * @param  timeout  <code>int</code>
+   */
+  public void setTimeout(final int timeout)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting timeout: " + timeout);
+    }
+    this.timeout = new Integer(timeout);
+  }
+
+
+  /**
+   * This sets the bind DN to authenticate as before performing operations.
+   *
+   * @param  dn  <code>String</code> bind DN
+   */
+  public void setBindDn(final String dn)
+  {
+    checkImmutable();
+    checkStringInput(dn, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting bindDn: " + dn);
+    }
+    this.bindDn = dn;
+  }
+
+
+  /**
+   * This sets the username of the service user. user must be a fully qualified
+   * DN.
+   *
+   * @param  user  <code>String</code> username
+   *
+   * @deprecated  use {@link #setBindDn(String)} instead
+   */
+  @Deprecated
+  public void setServiceUser(final String user)
+  {
+    this.setBindDn(user);
+  }
+
+
+  /**
+   * This sets the credential of the bind DN.
+   *
+   * @param  credential  <code>Object</code>
+   */
+  public void setBindCredential(final Object credential)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      if (this.getLogCredentials()) {
+        this.logger.trace("setting bindCredential: " + credential);
+      } else {
+        this.logger.trace("setting bindCredential: <suppressed>");
+      }
+    }
+    this.bindCredential = credential;
+  }
+
+
+  /**
+   * This sets the credential of the service user.
+   *
+   * @param  credential  <code>Object</code>
+   *
+   * @deprecated  use {@link #setBindCredential(Object)} instead
+   */
+  @Deprecated
+  public void setServiceCredential(final Object credential)
+  {
+    this.setBindCredential(credential);
+  }
+
+
+  /**
+   * This sets the username and credential of the service user. user must be a
+   * fully qualified DN.
+   *
+   * @param  user  <code>String</code> service user dn
+   * @param  credential  <code>Object</code>
+   *
+   * @deprecated  use {@link #setBindDn(String)} and {@link
+   * #setBindCredential(Object)} instead
+   */
+  @Deprecated
+  public void setService(final String user, final Object credential)
+  {
+    this.setBindDn(user);
+    this.setBindCredential(credential);
+  }
+
+
+  /**
+   * This sets the base dn for the <code>LdapConfig</code>.
+   *
+   * @param  base  <code>String</code> base dn
+   *
+   * @deprecated  use {@link #setBaseDn(String)}
+   */
+  public void setBase(final String base)
+  {
+    this.setBaseDn(base);
+  }
+
+
+  /**
+   * This sets the base dn for the <code>LdapConfig</code>.
+   *
+   * @param  baseDn  <code>String</code> base dn
+   */
+  public void setBaseDn(final String baseDn)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting baseDn: " + baseDn);
+    }
+    this.baseDn = baseDn;
+  }
+
+
+  /**
+   * This sets the search scope for the <code>LdapConfig</code>.
+   *
+   * @param  searchScope  <code>SearchScope</code>
+   */
+  public void setSearchScope(final SearchScope searchScope)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting searchScope: " + searchScope);
+    }
+    this.searchScope = searchScope;
+  }
+
+
+  /**
+   * This sets the security level for the <code>LdapConfig</code>.
+   *
+   * @param  authtype  <code>String</code> security level
+   */
+  public void setAuthtype(final String authtype)
+  {
+    checkImmutable();
+    checkStringInput(authtype, false);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting authtype: " + authtype);
+    }
+    this.authtype = authtype;
+  }
+
+
+  /**
+   * This specifies whether or not to force this <code>LdapConfig</code> to
+   * require an authoritative source.
+   *
+   * @param  authoritative  <code>boolean</code>
+   */
+  public void setAuthoritative(final boolean authoritative)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting authoritative: " + authoritative);
+    }
+    this.authoritative = authoritative;
+  }
+
+
+  /**
+   * This sets the maximum amount of time in milliseconds that search operations
+   * will block.
+   *
+   * @param  timeLimit  <code>int</code>
+   */
+  public void setTimeLimit(final int timeLimit)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting timeLimit: " + timeLimit);
+    }
+    this.timeLimit = new Integer(timeLimit);
+  }
+
+
+  /**
+   * This sets the maximum number of entries that search operations will return.
+   *
+   * @param  countLimit  <code>long</code>
+   */
+  public void setCountLimit(final long countLimit)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting countLimit: " + countLimit);
+    }
+    this.countLimit = new Long(countLimit);
+  }
+
+
+  /**
+   * This sets the results size to use when the PagedResultsControl is invoked.
+   *
+   * @param  pageSize  <code>int</code>
+   */
+  public void setPagedResultsSize(final int pageSize)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting pagedResultsSize: " + pageSize);
+    }
+    this.pagedResultsSize = new Integer(pageSize);
+  }
+
+
+  /**
+   * This sets the number of times that ldap operations will be retried if a
+   * communication exception occurs.
+   *
+   * @param  operationRetry  <code>int</code>
+   */
+  public void setOperationRetry(final int operationRetry)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting operationRetry: " + operationRetry);
+    }
+    this.operationRetry = new Integer(operationRetry);
+  }
+
+
+  /**
+   * This sets the exception types to retry operations on.
+   *
+   * @param  exceptions  <code>Class[]</code>
+   */
+  public void setOperationRetryExceptions(final Class<?>[] exceptions)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "setting operationRetryExceptions: " + Arrays.toString(exceptions));
+    }
+    this.operationRetryExceptions = exceptions;
+  }
+
+
+  /**
+   * This sets the amount of time in milliseconds that operations should wait
+   * before retrying.
+   *
+   * @param  wait  <code>long</code>
+   */
+  public void setOperationRetryWait(final long wait)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting operationRetryWait: " + wait);
+    }
+    this.operationRetryWait = new Long(wait);
+  }
+
+
+  /**
+   * This sets the factor by which to multiply the operation retry wait time.
+   * This allows clients to progressively delay each retry. The formula for
+   * backoff is (wait * backoff * attempt). So a wait time of 2s with a backoff
+   * of 3 will delay by 6s, then 12s, then 18s, and so forth.
+   *
+   * @param  backoff  <code>int</code>
+   */
+  public void setOperationRetryBackoff(final int backoff)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting operationRetryBackoff: " + backoff);
+    }
+    this.operationRetryBackoff = new Integer(backoff);
+  }
+
+
+  /**
+   * This specifies whether or not to force this <code>LdapConfig</code> to link
+   * dereferencing during searches.
+   *
+   * @param  derefLinkFlag  <code>boolean</code>
+   */
+  public void setDerefLinkFlag(final boolean derefLinkFlag)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting derefLinkFlag: " + derefLinkFlag);
+    }
+    this.derefLinkFlag = derefLinkFlag;
+  }
+
+
+  /**
+   * This specifies whether or not to force this <code>LdapConfig</code> to
+   * return objects for searches.
+   *
+   * @param  returningObjFlag  <code>boolean</code>
+   */
+  public void setReturningObjFlag(final boolean returningObjFlag)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting returningObjFlag: " + returningObjFlag);
+    }
+    this.returningObjFlag = returningObjFlag;
+  }
+
+
+  /**
+   * This sets the batch size for the <code>LdapConfig</code>. A value of -1
+   * indicates to use the provider default.
+   *
+   * @param  batchSize  <code>int</code> batch size to use when returning
+   * results
+   */
+  public void setBatchSize(final int batchSize)
+  {
+    checkImmutable();
+    if (batchSize == -1) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("setting batchSize: " + null);
+      }
+      this.batchSize = null;
+    } else {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("setting batchSize: " + batchSize);
+      }
+      this.batchSize = new Integer(batchSize);
+    }
+  }
+
+
+  /**
+   * This sets the dns url for the <code>LdapConfig</code>.
+   *
+   * @param  dnsUrl  <code>String</code>
+   */
+  public void setDnsUrl(final String dnsUrl)
+  {
+    checkImmutable();
+    checkStringInput(dnsUrl, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting dnsUrl: " + dnsUrl);
+    }
+    this.dnsUrl = dnsUrl;
+  }
+
+
+  /**
+   * This sets the preferred language for the <code>LdapConfig</code>.
+   *
+   * @param  language  <code>String</code> defined by RFC 1766
+   */
+  public void setLanguage(final String language)
+  {
+    checkImmutable();
+    checkStringInput(language, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting language: " + language);
+    }
+    this.language = language;
+  }
+
+
+  /**
+   * This specifies how the <code>LdapConfig</code> should handle referrals.
+   * referral must be one of: "throw", "ignore", or "follow".
+   *
+   * @param  referral  <code>String</code>
+   */
+  public void setReferral(final String referral)
+  {
+    checkImmutable();
+    checkStringInput(referral, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting referral: " + referral);
+    }
+    this.referral = referral;
+  }
+
+
+  /**
+   * This specifies how the <code>LdapConfig</code> should handle aliases.
+   * derefAliases must be one of: "always", "never", "finding", or "searching".
+   *
+   * @param  derefAliases  <code>String</code>
+   */
+  public void setDerefAliases(final String derefAliases)
+  {
+    checkImmutable();
+    checkStringInput(derefAliases, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting derefAliases: " + derefAliases);
+    }
+    this.derefAliases = derefAliases;
+  }
+
+
+  /**
+   * This specifies additional attributes that should be considered binary.
+   * Attributes should be space delimited.
+   *
+   * @param  binaryAttributes  <code>String</code>
+   */
+  public void setBinaryAttributes(final String binaryAttributes)
+  {
+    checkImmutable();
+    checkStringInput(binaryAttributes, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting binaryAttributes: " + binaryAttributes);
+    }
+    this.binaryAttributes = binaryAttributes;
+  }
+
+
+  /**
+   * This sets the handlers for processing search results.
+   *
+   * @param  handlers  <code>SearchResultHandler[]</code>
+   */
+  public void setSearchResultHandlers(final SearchResultHandler[] handlers)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "setting searchResultsHandlers: " + Arrays.toString(handlers));
+    }
+    this.searchResultHandlers = handlers;
+  }
+
+
+  /**
+   * This sets the exception types to ignore when handling results.
+   *
+   * @param  exceptions  <code>Class[]</code>
+   */
+  public void setHandlerIgnoreExceptions(final Class<?>[] exceptions)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "setting handlerIgnoreExceptions: " + Arrays.toString(exceptions));
+    }
+    this.handlerIgnoreExceptions = exceptions;
+  }
+
+
+  /**
+   * This specifies a SASL authorization id.
+   *
+   * @param  saslAuthorizationId  <code>String</code>
+   */
+  public void setSaslAuthorizationId(final String saslAuthorizationId)
+  {
+    checkImmutable();
+    checkStringInput(saslAuthorizationId, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting saslAuthorizationId: " + saslAuthorizationId);
+    }
+    this.saslAuthorizationId = saslAuthorizationId;
+  }
+
+
+  /**
+   * This specifies a SASL realm.
+   *
+   * @param  saslRealm  <code>String</code>
+   */
+  public void setSaslRealm(final String saslRealm)
+  {
+    checkImmutable();
+    checkStringInput(saslRealm, true);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting saslRealm: " + saslRealm);
+    }
+    this.saslRealm = saslRealm;
+  }
+
+
+  /**
+   * This specifies whether or not to force this <code>LdapConfig</code> to
+   * return only attribute types.
+   *
+   * @param  typesOnly  <code>boolean</code>
+   */
+  public void setTypesOnly(final boolean typesOnly)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting typesOnly: " + typesOnly);
+    }
+    this.typesOnly = typesOnly;
+  }
+
+
+  /** {@inheritDoc} */
+  public String getPropertiesDomain()
+  {
+    return PROPERTIES_DOMAIN;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setEnvironmentProperties(final String name, final String value)
+  {
+    checkImmutable();
+    if (name != null && value != null) {
+      if (PROPERTIES.hasProperty(name)) {
+        PROPERTIES.setProperty(this, name, value);
+      } else {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("setting property " + name + ": " + value);
+        }
+        this.additionalEnvironmentProperties.put(name, value);
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean hasEnvironmentProperty(final String name)
+  {
+    return PROPERTIES.hasProperty(name);
+  }
+
+
+  /**
+   * Create an instance of this class initialized with properties from the input
+   * stream. If the input stream is null, load properties from the default
+   * properties file.
+   *
+   * @param  is  to load properties from
+   *
+   * @return  <code>LdapConfig</code> initialized ldap config
+   */
+  public static LdapConfig createFromProperties(final InputStream is)
+  {
+    final LdapConfig ldapConfig = new LdapConfig();
+    LdapProperties properties = null;
+    if (is != null) {
+      properties = new LdapProperties(ldapConfig, is);
+    } else {
+      properties = new LdapProperties(ldapConfig);
+      properties.useDefaultPropertiesFile();
+    }
+    properties.configure();
+    return ldapConfig;
+  }
+
+
+  /**
+   * This sets whether authentication credentials will be logged.
+   *
+   * @param  log  <code>boolean</code>
+   */
+  public void setLogCredentials(final boolean log)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting logCredentials: " + log);
+    }
+    this.logCredentials = log;
+  }
+
+
+  /**
+   * This sets this <code>LdapConfig</code> to use the SSL protocol for
+   * connections.
+   *
+   * @param  ssl  <code>boolean</code>
+   */
+  public void setSsl(final boolean ssl)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting ssl: " + ssl);
+    }
+    this.ssl = ssl;
+  }
+
+
+  /**
+   * This sets this <code>LdapConfig</code> to use the TLS protocol for
+   * connections. Specifically it sets the connection handler to use {@link
+   * TlsConnectionHandler}.
+   *
+   * @param  tls  <code>boolean</code>
+   */
+  public void setTls(final boolean tls)
+  {
+    if (tls) {
+      this.setConnectionHandler(new TlsConnectionHandler());
+    } else {
+      this.setConnectionHandler(new DefaultConnectionHandler());
+    }
+  }
+
+
+  /**
+   * This returns a <code>SearchControls</code> object configured with this
+   * <code>LdapConfig</code>.
+   *
+   * @param  retAttrs  <code>String[]</code> attributes to return from search
+   *
+   * @return  <code>SearchControls</code>
+   */
+  public SearchControls getSearchControls(final String[] retAttrs)
+  {
+    final SearchControls ctls = new SearchControls();
+    ctls.setReturningAttributes(retAttrs);
+    ctls.setSearchScope(this.getSearchScope().ordinal());
+    ctls.setTimeLimit(this.getTimeLimit());
+    ctls.setCountLimit(this.getCountLimit());
+    ctls.setDerefLinkFlag(this.getDerefLinkFlag());
+    ctls.setReturningObjFlag(this.getReturningObjFlag());
+    return ctls;
+  }
+
+
+  /**
+   * This returns a <code>SearchControls</code> object configured to perform a
+   * LDAP compare operation.
+   *
+   * @return  <code>SearchControls</code>
+   */
+  public static SearchControls getCompareSearchControls()
+  {
+    final SearchControls ctls = new SearchControls();
+    ctls.setReturningAttributes(new String[0]);
+    ctls.setSearchScope(SearchScope.OBJECT.ordinal());
+    return ctls;
+  }
+
+
+  /**
+   * This sets this <code>LdapConfig</code> to print ASN.1 BER packets to the
+   * supplied <code>PrintStream</code>.
+   *
+   * @param  stream  <code>PrintStream</code>
+   */
+  public void setTracePackets(final PrintStream stream)
+  {
+    checkImmutable();
+    this.tracePackets = stream;
+  }
+
+
+  /**
+   * Provides a descriptive string representation of this instance.
+   *
+   * @return  String of the form $Classname at hashCode::env=$env.
+   */
+  @Override
+  public String toString()
+  {
+    return
+      String.format(
+        "%s@%d::env=%s",
+        this.getClass().getName(),
+        this.hashCode(),
+        this.getEnvironment());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/LdapConstants.java b/src/main/java/edu/vt/middleware/ldap/LdapConstants.java
new file mode 100644
index 0000000..d637470
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/LdapConstants.java
@@ -0,0 +1,352 @@
+/*
+  $Id: LdapConstants.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+/**
+ * <code>LdapConstants</code> contains all the constants needed for creating a
+ * <code>Ldap</code>. See
+ * http://java.sun.com/j2se/1.4.2/docs/guide/jndi/jndi-ldap.html or
+ * http://java.sun.com/j2se/1.4.2/docs/guide/jndi/spec/jndi/properties.html for
+ * more information on JNDI properties.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class LdapConstants
+{
+
+  /**
+   * The value of this property is a fully qualified class name of the factory
+   * class which creates the initial context for the LDAP service provider. The
+   * value of this constant is {@value}.
+   */
+  public static final String CONTEXT_FACTORY = "java.naming.factory.initial";
+
+  /**
+   * The value of this property is a string identifying the class name of a
+   * socket factory. The value of this constant is {@value}.
+   */
+  public static final String SOCKET_FACTORY = "java.naming.ldap.factory.socket";
+
+  /**
+   * The value of this property is a string specifying the authoritativeness of
+   * the service requested. The value of this constant is {@value}.
+   */
+  public static final String AUTHORITATIVE = "java.naming.authoritative";
+
+  /**
+   * The value of this property is a java.io.OutputStream object into which a
+   * hexadecimal dump of the incoming and outgoing LDAP ASN.1 BER packets is
+   * written. The value of this constant is {@value}.
+   */
+  public static final String TRACE = "com.sun.jndi.ldap.trace.ber";
+
+  /**
+   * The value of this property is a string that specifies the authentication
+   * mechanism(s) for the provider to use. The value of this constant is {@value
+   * }.
+   */
+  public static final String AUTHENTICATION =
+    "java.naming.security.authentication";
+
+  /**
+   * The value of this property is a string that specifies the identity of the
+   * principal to be authenticated. The value of this constant is {@value}.
+   */
+  public static final String PRINCIPAL = "java.naming.security.principal";
+
+  /**
+   * The value of this property is an object that specifies the credentials of
+   * the principal to be authenticated. The value of this constant is {@value}.
+   */
+  public static final String CREDENTIALS = "java.naming.security.credentials";
+
+  /**
+   * The value of this property is a string of decimal digits that specifies the
+   * batch size of search results returned by the server. The value of this
+   * constant is {@value}.
+   */
+  public static final String BATCH_SIZE = "java.naming.batchsize";
+
+  /**
+   * The value of this property is a string that specifies the DNS host and
+   * domain names. The value of this constant is {@value}.
+   */
+  public static final String DNS_URL = "java.naming.dns.url";
+
+  /**
+   * The value of this property is a string language tag according to RFC 1766.
+   * The value of this constant is {@value}.
+   */
+  public static final String LANGUAGE = "java.naming.language";
+
+  /**
+   * The value of this property is a string that specifies how referrals shall
+   * be handled by the provider. The value of this constant is {@value}.
+   */
+  public static final String REFERRAL = "java.naming.referral";
+
+  /**
+   * The value of this property is a string that specifies how aliases shall be
+   * handled by the provider. The value of this constant is {@value}.
+   */
+  public static final String DEREF_ALIASES = "java.naming.ldap.derefAliases";
+
+  /**
+   * The value of this property is a string that specifies additional binary
+   * attributes. The value of this constant is {@value}.
+   */
+  public static final String BINARY_ATTRIBUTES =
+    "java.naming.ldap.attributes.binary";
+
+  /**
+   * The value of this property is a string that specifies a SASL authorization
+   * id. The value of this constant is {@value}.
+   */
+  public static final String SASL_AUTHORIZATION_ID =
+    "java.naming.security.sasl.authorizationId";
+
+  /**
+   * The value of this property is a string that specifies a SASL realm. The
+   * value of this constant is {@value}.
+   */
+  public static final String SASL_REALM = "java.naming.security.sasl.realm";
+
+  /**
+   * The value of this property is a string that specifies to only return
+   * attribute type names, no values. The value of this constant is {@value}.
+   */
+  public static final String TYPES_ONLY = "java.naming.ldap.typesOnly";
+
+  /**
+   * The value of this property is a string that specifies the security protocol
+   * for the provider to use. The value of this constant is {@value}.
+   */
+  public static final String PROTOCOL = "java.naming.security.protocol";
+
+  /**
+   * The value of this property is a string that specifies the protocol version
+   * for the provider. The value of this constant is {@value}.
+   */
+  public static final String VERSION = "java.naming.ldap.version";
+
+  /**
+   * The value of this property is a URL string that specifies the hostname and
+   * port number of the LDAP server, and the root distinguished name of the
+   * naming context to use. The value of this constant is {@value}.
+   */
+  public static final String PROVIDER_URL = "java.naming.provider.url";
+
+  /**
+   * The value of this property is a string that specifies the time in
+   * milliseconds that a connection attempt will abort if the connection cannot
+   * be made. The value of this constant is {@value}.
+   */
+  public static final String TIMEOUT = "com.sun.jndi.ldap.connect.timeout";
+
+  /**
+   * Value passed to PROTOCOL to use SSL.
+   * The value of this constant is {@value}.
+   */
+  public static final String SSL_PROTOCOL = "ssl";
+
+  /**
+   * Value passed to AUTHENTICATION to use simple authentication. The value of
+   * this constant is {@value}.
+   */
+  public static final String SIMPLE_AUTHTYPE = "simple";
+
+  /**
+   * Value passed to AUTHENTICATION to use simple authentication. The value of
+   * this constant is {@value}.
+   */
+  public static final String STRONG_AUTHTYPE = "strong";
+
+  /**
+   * Value passed to AUTHENTICATION to use none authentication The value of this
+   * constant is {@value}.
+   */
+  public static final String NONE_AUTHTYPE = "none";
+
+  /**
+   * Value passed to VERSION to use ldap version 3 controls The value of this
+   * constant is {@value}.
+   */
+  public static final String VERSION_THREE = "3";
+
+  /** Ldap scheme, the value of this constant is {@value}. */
+  public static final String PROVIDER_URL_SCHEME = "ldap";
+
+  /** Secure ldap scheme, the value of this constant is {@value}. */
+  public static final String PROVIDER_URL_SSL_SCHEME = "ldaps";
+
+  /**
+   * URL prefix used for constructing URLs. The value of this constant is
+   * {@value}.
+   */
+  public static final String PROVIDER_URL_PREFIX = "://";
+
+  /**
+   * URL separator used for constructing URLs. The value of this constant is
+   * {@value}.
+   */
+  public static final String PROVIDER_URL_SEPARATOR = ":";
+
+  /**
+   * Ldap command which returns a list of supported SASL mechanisms. The value
+   * of this constant is {@value}.
+   */
+  public static final String SUPPORTED_SASL_MECHANISMS =
+    "supportedSASLMechanisms";
+
+  /**
+   * Ldap command which returns a list of supported controls. The value of this
+   * constant is {@value}.
+   */
+  public static final String SUPPORTED_CONTROL = "supportedcontrol";
+
+  /**
+   * Value passed to AUTHENTICATION to use SASL authentication. The value of
+   * this constant is {@value}.
+   */
+  public static final String SASL_MECHANISM_EXTERNAL = "EXTERNAL";
+
+  /**
+   * Value passed to AUTHENTICATION to use DIGEST-MD5 authentication. The value
+   * of this constant is {@value}.
+   */
+  public static final String SASL_MECHANISM_DIGEST_MD5 = "DIGEST-MD5";
+
+  /**
+   * Value passed to AUTHENTICATION to use CRAM-MD5 authentication. The value of
+   * this constant is {@value}.
+   */
+  public static final String SASL_MECHANISM_CRAM_MD5 = "CRAM-MD5";
+
+  /**
+   * Value passed to AUTHENTICATION to use GSS-API authentication. The value of
+   * this constant is {@value}.
+   */
+  public static final String SASL_MECHANISM_GSS_API = "GSSAPI";
+
+  /** List of supported SASL Mechanisms. */
+  public static final String[] SASL_MECHANISMS = new String[] {
+    SASL_MECHANISM_EXTERNAL,
+    SASL_MECHANISM_DIGEST_MD5,
+    SASL_MECHANISM_CRAM_MD5,
+    SASL_MECHANISM_GSS_API,
+  };
+
+  /** Default context factory, value of this constant is {@value}. */
+  public static final String DEFAULT_CONTEXT_FACTORY =
+    "com.sun.jndi.ldap.LdapCtxFactory";
+
+  /** Default base DN, value of this constant is {@value}. */
+  public static final String DEFAULT_BASE_DN = "";
+
+  /**
+   * Default timeout, -1 means use provider setting. The value of this constant
+   * is {@value}.
+   */
+  public static final int DEFAULT_TIMEOUT = -1;
+
+  /** Default authentication type, the value of this constant is {@value}. */
+  public static final String DEFAULT_AUTHTYPE = SIMPLE_AUTHTYPE;
+
+  /**
+   * Default time limit, 0 means wait indefinitely. The value of this constant
+   * is {@value}.
+   */
+  public static final int DEFAULT_TIME_LIMIT = 0;
+
+  /**
+   * Default count limit, 0 means return all results. The value of this constant
+   * is {@value}.
+   */
+  public static final long DEFAULT_COUNT_LIMIT = 0;
+
+  /** Default paged results size. The value of this constant is {@value}. */
+  public static final int DEFAULT_PAGED_RESULTS_SIZE = 0;
+
+  /**
+   * Default batch size, -1 means use provider setting. The value of this
+   * constant is {@value}.
+   */
+  public static final int DEFAULT_BATCH_SIZE = -1;
+
+  /** Default authoritative value, the value of this constant is {@value}. */
+  public static final boolean DEFAULT_AUTHORITATIVE = false;
+
+  /** Default type only value, the value of this constant is {@value}. */
+  public static final boolean DEFAULT_TYPES_ONLY = false;
+
+  /** Default ignore case value, value of this constant is {@value}. */
+  public static final boolean DEFAULT_IGNORE_CASE = true;
+
+  /** Default ldap port, the value of this constant is {@value}. */
+  public static final String DEFAULT_PORT = "389";
+
+  /** Default ldaps port, the value of this constant is {@value}. */
+  public static final String DEFAULT_SSL_PORT = "636";
+
+  /** Whether to use SSL by default, the value of this constant is {@value}. */
+  public static final boolean DEFAULT_USE_SSL = false;
+
+  /**
+   * Whether to log authentication credentials. The value of this constant is
+   * {@value}.
+   */
+  public static final boolean DEFAULT_LOG_CREDENTIALS = false;
+
+  /**
+   * Default userfield field used by Authenticator. The value of this constant
+   * is {@value}.
+   */
+  public static final String DEFAULT_USER_FIELD = "uid";
+
+  /**
+   * Whether Authenticator should throw an exception if multiple DNs are found
+   * by {@link edu.vt.middleware.ldap.auth.Authenticator#getDn(String)}. The
+   * value of this constant is {@value}.
+   */
+  public static final boolean DEFAULT_ALLOW_MULTIPLE_DNS = false;
+
+  /**
+   * Default character set for creating strings. The value of this constant is
+   * {@value}.
+   */
+  public static final String DEFAULT_CHARSET = "UTF-8";
+
+  /**
+   * Default number of times to retry an operation on failure. The value of this
+   * constant is {@value}.
+   */
+  public static final int DEFAULT_OPERATION_RETRY = 1;
+
+  /**
+   * Default amount of time to wait between operation retries. The value of this
+   * constant is {@value}.
+   */
+  public static final long DEFAULT_OPERATION_RETRY_WAIT = 0;
+
+  /**
+   * Default factor to multiply the operation retry wait by. The value of this
+   * constant is {@value}.
+   */
+  public static final int DEFAULT_OPERATION_RETRY_BACKOFF = 0;
+
+
+  /** Default constructor. */
+  private LdapConstants() {}
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/LdapSearch.java b/src/main/java/edu/vt/middleware/ldap/LdapSearch.java
new file mode 100644
index 0000000..a58f926
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/LdapSearch.java
@@ -0,0 +1,172 @@
+/*
+  $Id: LdapSearch.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Iterator;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.bean.LdapBeanFactory;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import edu.vt.middleware.ldap.pool.LdapPool;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>LdapSearch</code> queries an LDAP and returns the result. Each instance
+ * of <code>LdapSearch</code> maintains it's own pool of LDAP connections.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdapSearch
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Ldap object to use for searching. */
+  protected LdapPool<Ldap> pool;
+
+  /** Ldap bean factory. */
+  protected LdapBeanFactory beanFactory = LdapBeanProvider.getLdapBeanFactory();
+
+
+  /**
+   * This creates a new <code>LdapSearch</code> with the supplied pool.
+   *
+   * @param  pool  <code>LdapPool</code>
+   */
+  public LdapSearch(final LdapPool<Ldap> pool)
+  {
+    this.pool = pool;
+  }
+
+
+  /**
+   * Returns the factory for creating ldap beans.
+   *
+   * @return  <code>LdapBeanFactory</code>
+   */
+  public LdapBeanFactory getLdapBeanFactory()
+  {
+    return this.beanFactory;
+  }
+
+
+  /**
+   * Sets the factory for creating ldap beans.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public void setLdapBeanFactory(final LdapBeanFactory lbf)
+  {
+    if (lbf != null) {
+      this.beanFactory = lbf;
+    }
+  }
+
+
+  /**
+   * This will perform an LDAP search with the supplied query and return
+   * attributes.
+   *
+   * @param  query  <code>String</code> to search for
+   * @param  attrs  <code>String[]</code> to return
+   *
+   * @return  <code>Iterator</code> of search results
+   *
+   * @throws  NamingException  if an error occurs while searching
+   */
+  public Iterator<SearchResult> search(final String query, final String[] attrs)
+    throws NamingException
+  {
+    Iterator<SearchResult> queryResults = null;
+    if (query != null) {
+      try {
+        Ldap ldap = null;
+        try {
+          ldap = this.pool.checkOut();
+          queryResults = ldap.search(new SearchFilter(query), attrs);
+        } catch (NamingException e) {
+          if (this.logger.isErrorEnabled()) {
+            this.logger.error("Error attempting LDAP search", e);
+          }
+          throw e;
+        } finally {
+          this.pool.checkIn(ldap);
+        }
+      } catch (Exception e) {
+        if (this.logger.isErrorEnabled()) {
+          this.logger.error("Error using LDAP pool", e);
+        }
+      }
+    }
+    return queryResults;
+  }
+
+
+  /**
+   * This will perform an LDAP search with the supplied query and return
+   * attributes. The results will be written to the supplied <code>
+   * Writer</code>.
+   *
+   * @param  query  <code>String</code> to search for
+   * @param  attrs  <code>String[]</code> to return
+   * @param  writer  <code>Writer</code> to write to
+   *
+   * @throws  NamingException  if an error occurs while searching
+   * @throws  IOException  if an error occurs while writing search results
+   */
+  public void search(
+    final String query,
+    final String[] attrs,
+    final Writer writer)
+    throws NamingException, IOException
+  {
+    final LdapResult lr = this.beanFactory.newLdapResult();
+    lr.addEntries(this.search(query, attrs));
+    writer.write(lr.toString());
+    writer.flush();
+  }
+
+
+  /**
+   * Empties the underlying ldap pool, closing all connections. See {@link
+   * LdapPool#close()}.
+   */
+  public void close()
+  {
+    this.pool.close();
+  }
+
+
+  /**
+   * 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();
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/LdapUtil.java b/src/main/java/edu/vt/middleware/ldap/LdapUtil.java
new file mode 100644
index 0000000..33f0ee4
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/LdapUtil.java
@@ -0,0 +1,222 @@
+/*
+  $Id: LdapUtil.java 2217 2012-01-23 19:56:35Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2217 $
+  Updated: $Date: 2012-01-23 19:56:35 +0000 (Mon, 23 Jan 2012) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.util.regex.Pattern;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>LdapUtil</code> provides helper methods for <code>Ldap</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2217 $ $Date: 2012-01-23 19:56:35 +0000 (Mon, 23 Jan 2012) $
+ */
+public final class LdapUtil
+{
+
+  /** Size of buffer in bytes to use when reading files. */
+  private static final int READ_BUFFER_SIZE = 128;
+
+  /** Pattern to match ipv4 addresses. */
+  private static final Pattern IPV4_PATTERN =
+    Pattern.compile(
+      "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)" +
+      "(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");
+
+  /** Pattern to match ipv6 addresses. */
+  private static final Pattern IPV6_STD_PATTERN =
+    Pattern.compile("^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");
+
+  /** Pattern to match ipv6 hex compressed addresses. */
+  private static final Pattern IPV6_HEX_COMPRESSED_PATTERN =
+    Pattern.compile(
+      "^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::" +
+      "((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$");
+
+
+  /** Default constructor. */
+  private LdapUtil() {}
+
+
+  /**
+   * This checks a credential to ensure it is the right type and it is not
+   * empty. A credential can be of type String, char[], or byte[].
+   *
+   * @param  credential  <code>Object</code> to check
+   *
+   * @return  <code>boolean</code> - whether the credential is valid
+   */
+  public static boolean checkCredential(final Object credential)
+  {
+    boolean answer = false;
+    if (credential != null) {
+      if (credential instanceof String) {
+        final String string = (String) credential;
+        if (!"".equals(string)) {
+          answer = true;
+        }
+      } else if (credential instanceof char[]) {
+        final char[] array = (char[]) credential;
+        if (array.length != 0) {
+          answer = true;
+        }
+      } else if (credential instanceof byte[]) {
+        final byte[] array = (byte[]) credential;
+        if (array.length != 0) {
+          answer = true;
+        }
+      }
+    }
+    return answer;
+  }
+
+
+  /**
+   * This will convert the supplied value to a base64 encoded string. Returns
+   * null if the bytes cannot be encoded.
+   *
+   * @param  value  <code>byte[]</code> to base64 encode
+   *
+   * @return  <code>String</code>
+   */
+  public static String base64Encode(final byte[] value)
+  {
+    String encodedValue = null;
+    if (value != null) {
+      try {
+        encodedValue = new String(
+          Base64.encodeBase64(value),
+          LdapConstants.DEFAULT_CHARSET);
+      } catch (UnsupportedEncodingException e) {
+        final Log logger = LogFactory.getLog(LdapUtil.class);
+        if (logger.isErrorEnabled()) {
+          logger.error(
+            "Could not encode value using " + LdapConstants.DEFAULT_CHARSET);
+        }
+      }
+    }
+    return encodedValue;
+  }
+
+
+  /**
+   * This will convert the supplied value to a base64 encoded string. Returns
+   * null if the string cannot be encoded.
+   *
+   * @param  value  <code>String</code> to base64 encode
+   *
+   * @return  <code>String</code>
+   */
+  public static String base64Encode(final String value)
+  {
+    String encodedValue = null;
+    if (value != null) {
+      try {
+        encodedValue = base64Encode(
+          value.getBytes(LdapConstants.DEFAULT_CHARSET));
+      } catch (UnsupportedEncodingException e) {
+        final Log logger = LogFactory.getLog(LdapUtil.class);
+        if (logger.isErrorEnabled()) {
+          logger.error(
+            "Could not encode value using " + LdapConstants.DEFAULT_CHARSET);
+        }
+      }
+    }
+    return encodedValue;
+  }
+
+
+  /**
+   * This will decode the supplied value as a base64 encoded string to a byte[].
+   *
+   * @param  value  <code>Object</code> to base64 encode
+   *
+   * @return  <code>String</code>
+   */
+  public static byte[] base64Decode(final String value)
+  {
+    byte[] decodedValue = null;
+    if (value != null) {
+      decodedValue = Base64.decodeBase64(value.getBytes());
+    }
+    return decodedValue;
+  }
+
+
+  /**
+   * Reads the data at the supplied URL and returns it as a byte array.
+   *
+   * @param  url  <code>URL</code> to read
+   *
+   * @return  <code>byte[]</code> read from URL
+   *
+   * @throws  IOException  if an error occurs reading data
+   */
+  public static byte[] readURL(final URL url)
+    throws IOException
+  {
+    return readInputStream(url.openStream());
+  }
+
+
+  /**
+   * Reads the data in the supplied stream and returns it as a byte array.
+   *
+   * @param  is  <code>InputStream</code> to read
+   *
+   * @return  <code>byte[]</code> read from the stream
+   *
+   * @throws  IOException  if an error occurs reading data
+   */
+  public static byte[] readInputStream(final InputStream is)
+    throws IOException
+  {
+    final ByteArrayOutputStream data = new ByteArrayOutputStream();
+    try {
+      final byte[] buffer = new byte[READ_BUFFER_SIZE];
+      int length;
+      while ((length = is.read(buffer)) != -1) {
+        data.write(buffer, 0, length);
+      }
+    } finally {
+      is.close();
+      data.close();
+    }
+    return data.toByteArray();
+  }
+
+
+  /**
+   * Returns whether the supplied string represents an IP address. Matches both
+   * IPv4 and IPv6 addresses.
+   *
+   * @param  s  to match
+   *
+   * @return  whether the supplied string represents an IP address
+   */
+  public static boolean isIPAddress(final String s)
+  {
+    return s != null &&
+      (IPV4_PATTERN.matcher(s).matches() ||
+       IPV6_STD_PATTERN.matcher(s).matches() ||
+       IPV6_HEX_COMPRESSED_PATTERN.matcher(s).matches());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/SearchFilter.java b/src/main/java/edu/vt/middleware/ldap/SearchFilter.java
new file mode 100644
index 0000000..dcb7fde
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/SearchFilter.java
@@ -0,0 +1,147 @@
+/*
+  $Id: SearchFilter.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * <code>SearchFilter</code> provides a bean for a filter and it's arguments.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class SearchFilter
+{
+
+  /** filter. */
+  private String filter;
+
+  /** filter arguments. */
+  private List<Object> filterArgs = new ArrayList<Object>();
+
+
+  /** Default constructor. */
+  public SearchFilter() {}
+
+
+  /**
+   * Creates a new search filter with the supplied filter.
+   *
+   * @param  s  to set filter
+   */
+  public SearchFilter(final String s)
+  {
+    this.filter = s;
+  }
+
+
+  /**
+   * Creates a new string search filter with the supplied filter and arguments.
+   *
+   * @param  s  to set filter
+   * @param  o  to set filter arguments
+   */
+  public SearchFilter(final String s, final List<?> o)
+  {
+    this.setFilter(s);
+    this.setFilterArgs(o);
+  }
+
+
+  /**
+   * Creates a new search filter with the supplied filter and arguments.
+   *
+   * @param  s  to set filter
+   * @param  o  to set filter arguments
+   */
+  public SearchFilter(final String s, final Object[] o)
+  {
+    this.setFilter(s);
+    this.setFilterArgs(o);
+  }
+
+
+  /**
+   * Gets the filter.
+   *
+   * @return  filter
+   */
+  public String getFilter()
+  {
+    return this.filter;
+  }
+
+
+  /**
+   * Sets the filter.
+   *
+   * @param  s  to set filter
+   */
+  public void setFilter(final String s)
+  {
+    this.filter = s;
+  }
+
+
+  /**
+   * Gets the filter arguments.
+   *
+   * @return  filter args
+   */
+  public List<Object> getFilterArgs()
+  {
+    return this.filterArgs;
+  }
+
+
+  /**
+   * Sets the filter arguments.
+   *
+   * @param  o  to set filter arguments
+   */
+  public void setFilterArgs(final List<?> o)
+  {
+    if (o != null) {
+      this.filterArgs.addAll(o);
+    }
+  }
+
+
+  /**
+   * Sets the filter arguments.
+   *
+   * @param  o  to set filter arguments
+   */
+  public void setFilterArgs(final Object[] o)
+  {
+    if (o != null) {
+      this.filterArgs.addAll(Arrays.asList(o));
+    }
+  }
+
+
+  /**
+   * This returns a string representation of this search filter.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return
+      String.format("filter=%s,filterArgs=%s", this.filter, this.filterArgs);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/AbstractAuthenticator.java b/src/main/java/edu/vt/middleware/ldap/auth/AbstractAuthenticator.java
new file mode 100644
index 0000000..434e670
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/AbstractAuthenticator.java
@@ -0,0 +1,242 @@
+/*
+  $Id: AbstractAuthenticator.java 1743 2010-11-19 17:00:18Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1743 $
+  Updated: $Date: 2010-11-19 17:00:18 +0000 (Fri, 19 Nov 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.util.Arrays;
+import javax.naming.AuthenticationException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import edu.vt.middleware.ldap.LdapConstants;
+import edu.vt.middleware.ldap.LdapUtil;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationCriteria;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationResultHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthorizationHandler;
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractAuthenticator</code> provides basic functionality for
+ * authenticating against an LDAP.
+ *
+ * @param  <T>  type of AuthenticatorConfig
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1743 $ $Date: 2010-11-19 17:00:18 +0000 (Fri, 19 Nov 2010) $
+ */
+public abstract class AbstractAuthenticator<T extends AuthenticatorConfig>
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Authenticator configuration environment. */
+  protected T config;
+
+
+  /**
+   * This will set the config parameters of this <code>Authenticator</code>.
+   *
+   * @param  authConfig  <code>AuthenticatorConfig</code>
+   */
+  public void setAuthenticatorConfig(final T authConfig)
+  {
+    if (this.config != null) {
+      this.config.checkImmutable();
+    }
+    this.config = authConfig;
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP with the supplied dn and
+   * credential. See {@link #authenticateAndAuthorize( String, Object, boolean,
+   * String[], AuthenticationResultHandler[], AuthorizationHandler[])}.
+   *
+   * @param  dn  <code>String</code> for bind
+   * @param  credential  <code>Object</code> for bind
+   * @param  authResultHandler  <code>AuthenticationResultHandler[]</code> to
+   * post process authentication results
+   * @param  authzHandler  <code>AuthorizationHandler[]</code> to process
+   * authorization after authentication
+   *
+   * @return  <code>boolean</code> - whether the bind succeeded
+   *
+   * @throws  NamingException  if the authentication fails for any other reason
+   * than invalid credentials
+   */
+  protected boolean authenticateAndAuthorize(
+    final String dn,
+    final Object credential,
+    final AuthenticationResultHandler[] authResultHandler,
+    final AuthorizationHandler[] authzHandler)
+    throws NamingException
+  {
+    boolean success = false;
+    try {
+      this.authenticateAndAuthorize(
+        dn,
+        credential,
+        false,
+        null,
+        authResultHandler,
+        authzHandler);
+      success = true;
+    } catch (AuthenticationException e) {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Authentication failed for dn: " + dn, e);
+      }
+    } catch (AuthorizationException e) {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Authorization failed for dn: " + dn, e);
+      }
+    }
+    return success;
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP with the supplied dn and
+   * credential. Authentication will never succeed if {@link
+   * AuthenticatorConfig#getAuthtype()} is set to 'none'. If retAttrs is null
+   * and searchAttrs is true then all user attributes will be returned. If
+   * retAttrs is an empty array and searchAttrs is true then no attributes will
+   * be returned. This method throws AuthenticationException if authentication
+   * fails and AuthorizationException if authorization fails.
+   *
+   * @param  dn  <code>String</code> for bind
+   * @param  credential  <code>Object</code> for bind
+   * @param  searchAttrs  <code>boolean</code> whether to perform attribute
+   * search
+   * @param  retAttrs  <code>String[]</code> user attributes to return
+   * @param  authResultHandler  <code>AuthenticationResultHandler[]</code> to
+   * post process authentication results
+   * @param  authzHandler  <code>AuthorizationHandler[]</code> to process
+   * authorization after authentication
+   *
+   * @return  <code>Attribute</code> - belonging to the supplied user, returns
+   * null if searchAttrs is false
+   *
+   * @throws  NamingException  if any of the ldap operations fail
+   * @throws  AuthenticationException  if authentication fails
+   * @throws  AuthorizationException  if authorization fails
+   */
+  protected Attributes authenticateAndAuthorize(
+    final String dn,
+    final Object credential,
+    final boolean searchAttrs,
+    final String[] retAttrs,
+    final AuthenticationResultHandler[] authResultHandler,
+    final AuthorizationHandler[] authzHandler)
+    throws NamingException
+  {
+    // check the authentication type
+    final String authtype = this.config.getAuthtype();
+    if (authtype.equalsIgnoreCase(LdapConstants.NONE_AUTHTYPE)) {
+      throw new AuthenticationException(
+        "Cannot authenticate dn, authtype is 'none'");
+    }
+
+    // check the credential
+    if (!LdapUtil.checkCredential(credential)) {
+      throw new AuthenticationException(
+        "Cannot authenticate dn, invalid credential");
+    }
+
+    // check the dn
+    if (dn == null || "".equals(dn)) {
+      throw new AuthenticationException("Cannot authenticate dn, invalid dn");
+    }
+
+    Attributes userAttributes = null;
+
+    // attempt to bind as this dn
+    final ConnectionHandler ch = this.config.getConnectionHandler()
+        .newInstance();
+    try {
+      final AuthenticationCriteria ac = new AuthenticationCriteria(dn);
+      ac.setCredential(credential);
+      try {
+        final AuthenticationHandler authHandler = this.config
+            .getAuthenticationHandler().newInstance();
+        authHandler.authenticate(ch, ac);
+        if (this.logger.isInfoEnabled()) {
+          this.logger.info("Authentication succeeded for dn: " + dn);
+        }
+      } catch (AuthenticationException e) {
+        if (this.logger.isInfoEnabled()) {
+          this.logger.info("Authentication failed for dn: " + dn);
+        }
+        if (authResultHandler != null && authResultHandler.length > 0) {
+          for (AuthenticationResultHandler ah : authResultHandler) {
+            ah.process(ac, false);
+          }
+        }
+        throw e;
+      }
+      // authentication succeeded, perform authorization if supplied
+      if (authzHandler != null && authzHandler.length > 0) {
+        for (AuthorizationHandler azh : authzHandler) {
+          try {
+            azh.process(ac, ch.getLdapContext());
+            if (this.logger.isInfoEnabled()) {
+              this.logger.info(
+                "Authorization succeeded for dn: " + dn + " with handler: " +
+                azh);
+            }
+          } catch (AuthenticationException e) {
+            if (this.logger.isInfoEnabled()) {
+              this.logger.info(
+                "Authorization failed for dn: " + dn + " with handler: " + azh);
+            }
+            if (authResultHandler != null && authResultHandler.length > 0) {
+              for (AuthenticationResultHandler ah : authResultHandler) {
+                ah.process(ac, false);
+              }
+            }
+            throw e;
+          }
+        }
+      }
+      if (searchAttrs) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Returning attributes: ");
+          this.logger.debug(
+            "    " +
+            (retAttrs == null ? "all attributes" : Arrays.toString(retAttrs)));
+        }
+        userAttributes = ch.getLdapContext().getAttributes(dn, retAttrs);
+      }
+      if (authResultHandler != null && authResultHandler.length > 0) {
+        for (AuthenticationResultHandler ah : authResultHandler) {
+          ah.process(ac, true);
+        }
+      }
+    } finally {
+      ch.close();
+    }
+
+    return userAttributes;
+  }
+
+
+  /** This will close the connection on the underlying DN resolver. */
+  public synchronized void close()
+  {
+    if (this.config.getDnResolver() != null) {
+      this.config.getDnResolver().close();
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/Authenticator.java b/src/main/java/edu/vt/middleware/ldap/auth/Authenticator.java
new file mode 100644
index 0000000..f57fb6c
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/Authenticator.java
@@ -0,0 +1,366 @@
+/*
+  $Id: Authenticator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationResultHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthorizationHandler;
+import edu.vt.middleware.ldap.auth.handler.CompareAuthorizationHandler;
+
+/**
+ * <code>Authenticator</code> contains functions for authenticating a user
+ * against an LDAP.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class Authenticator extends AbstractAuthenticator<AuthenticatorConfig>
+  implements Serializable
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -444519681288987247L;
+
+
+  /** Default constructor. */
+  public Authenticator() {}
+
+
+  /**
+   * This will create a new <code>Authenticator</code> with the supplied <code>
+   * AuthenticatorConfig</code>.
+   *
+   * @param  authConfig  <code>AuthenticatorConfig</code>
+   */
+  public Authenticator(final AuthenticatorConfig authConfig)
+  {
+    this.setAuthenticatorConfig(authConfig);
+  }
+
+
+  /**
+   * This returns the <code>AuthenticatorConfig</code> of the <code>
+   * Authenticator</code>.
+   *
+   * @return  <code>AuthenticatorConfig</code>
+   */
+  public AuthenticatorConfig getAuthenticatorConfig()
+  {
+    return this.config;
+  }
+
+
+  /**
+   * This will set the config parameters of this <code>Authenticator</code>
+   * using the default properties file, which must be located in your classpath.
+   */
+  public void loadFromProperties()
+  {
+    this.setAuthenticatorConfig(AuthenticatorConfig.createFromProperties(null));
+  }
+
+
+  /**
+   * This will set the config parameters of this <code>Authenticator</code>
+   * using the supplied input stream.
+   *
+   * @param  is  <code>InputStream</code>
+   */
+  public void loadFromProperties(final InputStream is)
+  {
+    this.setAuthenticatorConfig(AuthenticatorConfig.createFromProperties(is));
+  }
+
+
+  /**
+   * This will attempt to find the LDAP DN for the supplied user. {@link
+   * AuthenticatorConfig#dnResolver} is invoked to perform this operation.
+   *
+   * @param  user  <code>String</code> to find dn for
+   *
+   * @return  <code>String</code> - user's dn
+   *
+   * @throws  NamingException  an LDAP error occurs
+   */
+  public String getDn(final String user)
+    throws NamingException
+  {
+    return this.config.getDnResolver().resolve(user);
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP using parameters given by
+   * {@link AuthenticatorConfig#setUser} and {@link
+   * AuthenticatorConfig#setCredential}. See {@link #authenticate(String,
+   * Object)}.
+   *
+   * @return  <code>boolean</code> - whether the bind succeeded
+   *
+   * @throws  NamingException  if the authentication fails for any other reason
+   * than invalid credentials
+   */
+  public boolean authenticate()
+    throws NamingException
+  {
+    return
+      this.authenticate(this.config.getUser(), this.config.getCredential());
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP with the supplied user and
+   * credential. If {@link AuthenticatorConfig#setAuthorizationFilter} has been
+   * called, then it will be used to authorize the user by performing an ldap
+   * compare. See {@link #authenticate(String, Object, SearchFilter)}.
+   *
+   * @param  user  <code>String</code> username for bind
+   * @param  credential  <code>Object</code> credential for bind
+   *
+   * @return  <code>boolean</code> - whether the bind succeeded
+   *
+   * @throws  NamingException  if the authentication fails for any other reason
+   * than invalid credentials
+   */
+  public boolean authenticate(final String user, final Object credential)
+    throws NamingException
+  {
+    return
+      this.authenticate(
+        user,
+        credential,
+        new SearchFilter(
+          this.config.getAuthorizationFilter(),
+          this.config.getAuthorizationFilterArgs()));
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP with the supplied user and
+   * credential. If the supplied filter is not null it will be injected into a
+   * new instance of CompareAuthorizationHandler and set as the first
+   * AuthorizationHandler to execute. If {@link
+   * AuthenticatorConfig#setAuthenticationResultHandlers(
+   * AuthenticationResultHandler[])} has been called, then it will be used to
+   * post process authentication results. See {@link #authenticate(String,
+   * Object, AuthenticationResultHandler[], AuthorizationHandler[])}.
+   *
+   * @param  user  <code>String</code> username for bind
+   * @param  credential  <code>Object</code> credential for bind
+   * @param  filter  <code>SearchFilter</code> to authorize user
+   *
+   * @return  <code>boolean</code> - whether the bind succeeded
+   *
+   * @throws  NamingException  if the authentication fails for any other reason
+   * than invalid credentials
+   */
+  public boolean authenticate(
+    final String user,
+    final Object credential,
+    final SearchFilter filter)
+    throws NamingException
+  {
+    final List<AuthorizationHandler> authzHandler =
+      new ArrayList<AuthorizationHandler>();
+    if (filter != null && filter.getFilter() != null) {
+      authzHandler.add(new CompareAuthorizationHandler(filter));
+    }
+    if (this.config.getAuthorizationHandlers() != null) {
+      authzHandler.addAll(
+        Arrays.asList(this.config.getAuthorizationHandlers()));
+    }
+    return
+      this.authenticate(
+        user,
+        credential,
+        this.config.getAuthenticationResultHandlers(),
+        authzHandler.toArray(new AuthorizationHandler[0]));
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP with the supplied user and
+   * credential. The user's DN will be looked up before performing the bind by
+   * calling {@link DnResolver#resolve(String)}. See {@link
+   * #authenticateAndAuthorize(String, Object, AuthenticationResultHandler[],
+   * AuthorizationHandler[])}.
+   *
+   * @param  user  <code>String</code> username for bind
+   * @param  credential  <code>Object</code> credential for bind
+   * @param  authHandler  <code>AuthenticationResultHandler[]</code> to post
+   * process authentication results
+   * @param  authzHandler  <code>AuthorizationHandler[]</code> to process
+   * authorization after authentication
+   *
+   * @return  <code>boolean</code> - whether the bind succeeded
+   *
+   * @throws  NamingException  if the authentication fails for any other reason
+   * than invalid credentials
+   */
+  public boolean authenticate(
+    final String user,
+    final Object credential,
+    final AuthenticationResultHandler[] authHandler,
+    final AuthorizationHandler[] authzHandler)
+    throws NamingException
+  {
+    return
+      super.authenticateAndAuthorize(
+        this.getDn(user),
+        credential,
+        authHandler,
+        authzHandler);
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP using parameters given by
+   * {@link AuthenticatorConfig#setUser} and {@link
+   * AuthenticatorConfig#setCredential}. See {@link
+   * #authenticate(String,Object,String[])}
+   *
+   * @param  retAttrs  <code>String[]</code> attributes to return
+   *
+   * @return  <code>Attributes</code> - of authenticated user
+   *
+   * @throws  NamingException  if any of the ldap operations fail
+   */
+  public Attributes authenticate(final String[] retAttrs)
+    throws NamingException
+  {
+    return
+      this.authenticate(
+        this.config.getUser(),
+        this.config.getCredential(),
+        retAttrs);
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP with the supplied user and
+   * credential. If {@link AuthenticatorConfig#setAuthorizationFilter} has been
+   * called, then it will be used to authorize the user by performing an ldap
+   * compare. See {@link #authenticate(String, Object, SearchFilter, String[])}
+   *
+   * @param  user  <code>String</code> username for bind
+   * @param  credential  <code>Object</code> credential for bind
+   * @param  retAttrs  <code>String[]</code> to return
+   *
+   * @return  <code>Attributes</code> - of authenticated user
+   *
+   * @throws  NamingException  if any of the ldap operations fail
+   */
+  public Attributes authenticate(
+    final String user,
+    final Object credential,
+    final String[] retAttrs)
+    throws NamingException
+  {
+    return
+      this.authenticate(
+        user,
+        credential,
+        new SearchFilter(
+          this.config.getAuthorizationFilter(),
+          this.config.getAuthorizationFilterArgs()),
+        retAttrs);
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP with the supplied user and
+   * credential. If the supplied filter is not null it will be injected into a
+   * new instance of CompareAuthorizationHandler and set as the first
+   * AuthorizationHandler to execute. See {@link #authenticate(String, Object,
+   * String[], AuthenticationResultHandler[], AuthorizationHandler[])}.
+   *
+   * @param  user  <code>String</code> username for bind
+   * @param  credential  <code>Object</code> credential for bind
+   * @param  filter  <code>SearchFilter</code> to authorize user
+   * @param  retAttrs  <code>String[]</code> to return
+   *
+   * @return  <code>Attributes</code> - of authenticated user
+   *
+   * @throws  NamingException  if any of the ldap operations fail
+   */
+  public Attributes authenticate(
+    final String user,
+    final Object credential,
+    final SearchFilter filter,
+    final String[] retAttrs)
+    throws NamingException
+  {
+    final List<AuthorizationHandler> authzHandler =
+      new ArrayList<AuthorizationHandler>();
+    if (filter != null && filter.getFilter() != null) {
+      authzHandler.add(new CompareAuthorizationHandler(filter));
+    }
+    if (this.config.getAuthorizationHandlers() != null) {
+      authzHandler.addAll(
+        Arrays.asList(this.config.getAuthorizationHandlers()));
+    }
+    return
+      this.authenticate(
+        user,
+        credential,
+        retAttrs,
+        this.config.getAuthenticationResultHandlers(),
+        authzHandler.toArray(new AuthorizationHandler[0]));
+  }
+
+
+  /**
+   * This will authenticate by binding to the LDAP with the supplied user and
+   * credential. The user's DN will be looked up before performing the bind by
+   * calling {@link DnResolver#resolve(String)}. See {@link
+   * #authenticateAndAuthorize(String, Object, boolean, String[],
+   * AuthenticationResultHandler[], AuthorizationHandler[])}.
+   *
+   * @param  user  <code>String</code> username for bind
+   * @param  credential  <code>Object</code> credential for bind
+   * @param  retAttrs  <code>String[]</code> to return
+   * @param  authHandler  <code>AuthenticationResultHandler[]</code> to post
+   * process authentication results
+   * @param  authzHandler  <code>AuthorizationHandler[]</code> to process
+   * authorization after authentication
+   *
+   * @return  <code>Attributes</code> - of authenticated user
+   *
+   * @throws  NamingException  if any of the ldap operations fail
+   */
+  public Attributes authenticate(
+    final String user,
+    final Object credential,
+    final String[] retAttrs,
+    final AuthenticationResultHandler[] authHandler,
+    final AuthorizationHandler[] authzHandler)
+    throws NamingException
+  {
+    return
+      this.authenticateAndAuthorize(
+        this.getDn(user),
+        credential,
+        true,
+        retAttrs,
+        authHandler,
+        authzHandler);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/AuthenticatorCli.java b/src/main/java/edu/vt/middleware/ldap/auth/AuthenticatorCli.java
new file mode 100644
index 0000000..1f1fe03
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/AuthenticatorCli.java
@@ -0,0 +1,191 @@
+/*
+  $Id: AuthenticatorCli.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import javax.naming.directory.Attributes;
+import edu.vt.middleware.ldap.AbstractCli;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import edu.vt.middleware.ldap.dsml.Dsmlv1;
+import edu.vt.middleware.ldap.dsml.Dsmlv2;
+import edu.vt.middleware.ldap.ldif.Ldif;
+import edu.vt.middleware.ldap.props.LdapConfigPropertyInvoker;
+import org.apache.commons.cli.CommandLine;
+
+/**
+ * Command line interface for authenticator operations.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class AuthenticatorCli extends AbstractCli
+{
+
+  /** Name of operation provided by this class. */
+  private static final String COMMAND_NAME = "ldapauth";
+
+
+  /**
+   * CLI entry point method.
+   *
+   * @param  args  Command line arguments.
+   */
+  public static void main(final String[] args)
+  {
+    new AuthenticatorCli().performAction(args);
+  }
+
+
+  /** {@inheritDoc} */
+  protected void initOptions()
+  {
+    super.initOptions(
+      new LdapConfigPropertyInvoker(
+        AuthenticatorConfig.class,
+        AuthenticatorConfig.PROPERTIES_DOMAIN));
+  }
+
+
+  /**
+   * Initialize an AuthenticatorConfig with command line options.
+   *
+   * @param  line  Parsed command line arguments container.
+   *
+   * @return  <code>AuthenticatorConfig</code> that has been initialized
+   *
+   * @throws  Exception  On errors thrown by handler.
+   */
+  protected AuthenticatorConfig initAuthenticatorConfig(final CommandLine line)
+    throws Exception
+  {
+    final AuthenticatorConfig config = new AuthenticatorConfig();
+    this.initLdapProperties(config, line);
+    if (line.hasOption(OPT_TRACE)) {
+      config.setTracePackets(System.out);
+    }
+    if (config.getBindDn() != null && config.getBindCredential() == null) {
+      // prompt the user to enter a password
+      System.out.print(
+        "Enter password for service user " + config.getBindDn() + ": ");
+
+      final String pass = (new BufferedReader(new InputStreamReader(System.in)))
+          .readLine();
+      config.setBindCredential(pass);
+    }
+    if (config.getUser() == null) {
+      // prompt for a user name
+      System.out.print("Enter user name: ");
+
+      final String user = (new BufferedReader(new InputStreamReader(System.in)))
+          .readLine();
+      config.setUser(user);
+    }
+    if (config.getCredential() == null) {
+      // prompt the user to enter a password
+      System.out.print("Enter password for user " + config.getUser() + ": ");
+
+      final String pass = (new BufferedReader(new InputStreamReader(System.in)))
+          .readLine();
+      config.setCredential(pass);
+    }
+    return config;
+  }
+
+
+  /** {@inheritDoc} */
+  protected void dispatch(final CommandLine line)
+    throws Exception
+  {
+    if (line.hasOption(OPT_DSMLV1)) {
+      this.outputDsmlv1 = true;
+    } else if (line.hasOption(OPT_DSMLV2)) {
+      this.outputDsmlv2 = true;
+    }
+    if (line.hasOption(OPT_HELP)) {
+      printHelp();
+    } else {
+      authenticate(initAuthenticatorConfig(line), line.getArgs());
+    }
+  }
+
+
+  /**
+   * Executes the authenticate operation.
+   *
+   * @param  config  Authenticator configuration.
+   * @param  attrs  Ldap attributes to return
+   *
+   * @throws  Exception  On errors.
+   */
+  protected void authenticate(
+    final AuthenticatorConfig config,
+    final String[] attrs)
+    throws Exception
+  {
+    final Authenticator auth = new Authenticator();
+    auth.setAuthenticatorConfig(config);
+
+    Attributes results = null;
+    try {
+      if (attrs == null || attrs.length == 0) {
+        results = auth.authenticate(null);
+      } else {
+        results = auth.authenticate(attrs);
+      }
+      if (results != null && results.size() > 0) {
+        final LdapEntry entry = LdapBeanProvider.getLdapBeanFactory()
+            .newLdapEntry();
+        final LdapResult result = LdapBeanProvider.getLdapBeanFactory()
+            .newLdapResult();
+        result.addEntry(entry);
+        entry.setDn(auth.getDn(config.getUser()));
+
+        final LdapAttributes la = LdapBeanProvider.getLdapBeanFactory()
+            .newLdapAttributes();
+        la.addAttributes(results);
+        entry.setLdapAttributes(la);
+        if (this.outputDsmlv1) {
+          (new Dsmlv1()).outputDsml(
+            result.toSearchResults().iterator(),
+            new BufferedWriter(new OutputStreamWriter(System.out)));
+        } else if (this.outputDsmlv2) {
+          (new Dsmlv2()).outputDsml(
+            result.toSearchResults().iterator(),
+            new BufferedWriter(new OutputStreamWriter(System.out)));
+        } else {
+          (new Ldif()).outputLdif(
+            result.toSearchResults().iterator(),
+            new BufferedWriter(new OutputStreamWriter(System.out)));
+        }
+      }
+    } finally {
+      if (auth != null) {
+        auth.close();
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  protected String getCommandName()
+  {
+    return COMMAND_NAME;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/AuthenticatorConfig.java b/src/main/java/edu/vt/middleware/ldap/auth/AuthenticatorConfig.java
new file mode 100644
index 0000000..480ea84
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/AuthenticatorConfig.java
@@ -0,0 +1,555 @@
+/*
+  $Id: AuthenticatorConfig.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.io.InputStream;
+import java.util.Arrays;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.LdapConstants;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationResultHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthorizationHandler;
+import edu.vt.middleware.ldap.auth.handler.BindAuthenticationHandler;
+import edu.vt.middleware.ldap.props.LdapConfigPropertyInvoker;
+import edu.vt.middleware.ldap.props.LdapProperties;
+
+/**
+ * <code>AuthenticatorConfig</code> contains all the configuration data that the
+ * <code>Authenticator</code> needs to control authentication.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class AuthenticatorConfig extends LdapConfig
+{
+
+  /** Domain to look for ldap properties in, value is {@value}. */
+  public static final String PROPERTIES_DOMAIN = "edu.vt.middleware.ldap.auth.";
+
+  /** Invoker for ldap properties. */
+  private static final LdapConfigPropertyInvoker PROPERTIES =
+    new LdapConfigPropertyInvoker(AuthenticatorConfig.class, PROPERTIES_DOMAIN);
+
+  /** Directory user field. */
+  private String[] userField = new String[] {
+    LdapConstants.DEFAULT_USER_FIELD,
+  };
+
+  /** Filter for searching for the user. */
+  private String userFilter;
+
+  /** Filter arguments for searching for the user. */
+  private Object[] userFilterArgs;
+
+  /** User to authenticate. */
+  private String user;
+
+  /** Credential for authenticating user. */
+  private Object credential;
+
+  /** Filter for authorizing user. */
+  private String authorizationFilter;
+
+  /** Filter arguments for authorizing user. */
+  private Object[] authorizationFilterArgs;
+
+  /** Whether to throw an exception if multiple DNs are found. */
+  private boolean allowMultipleDns = LdapConstants.DEFAULT_ALLOW_MULTIPLE_DNS;
+
+  /** For finding LDAP DNs. */
+  private DnResolver dnResolver = new SearchDnResolver(this);
+
+  /** Handler to process authentication. */
+  private AuthenticationHandler authenticationHandler =
+    new BindAuthenticationHandler(this);
+
+  /** Handlers to process authentications. */
+  private AuthenticationResultHandler[] authenticationResultHandlers;
+
+  /** Handlers to process authorization. */
+  private AuthorizationHandler[] authorizationHandlers;
+
+
+  /** Default constructor. */
+  public AuthenticatorConfig()
+  {
+    this.setSearchScope(SearchScope.ONELEVEL);
+  }
+
+
+  /**
+   * This will create a new <code>AuthenticatorConfig</code> with the supplied
+   * ldap url and base Strings.
+   *
+   * @param  ldapUrl  <code>String</code> LDAP URL
+   * @param  baseDn  <code>String</code> LDAP base DN
+   */
+  public AuthenticatorConfig(final String ldapUrl, final String baseDn)
+  {
+    this();
+    this.setLdapUrl(ldapUrl);
+    this.setBaseDn(baseDn);
+  }
+
+
+  /**
+   * This returns the user field(s) of the <code>Authenticator</code>.
+   *
+   * @return  <code>String[]</code> - user field name(s)
+   */
+  public String[] getUserField()
+  {
+    return this.userField;
+  }
+
+
+  /**
+   * This returns the filter used to search for the user.
+   *
+   * @return  <code>String</code> - filter
+   */
+  public String getUserFilter()
+  {
+    return this.userFilter;
+  }
+
+
+  /**
+   * This returns the filter arguments used to search for the user.
+   *
+   * @return  <code>Object[]</code> - filter arguments
+   */
+  public Object[] getUserFilterArgs()
+  {
+    return this.userFilterArgs;
+  }
+
+
+  /**
+   * This returns the user of the <code>Authenticator</code>.
+   *
+   * @return  <code>String</code> - user name
+   */
+  public String getUser()
+  {
+    return this.user;
+  }
+
+
+  /**
+   * This returns the credential of the <code>Authenticator</code>.
+   *
+   * @return  <code>Object</code> - user credential
+   */
+  public Object getCredential()
+  {
+    return this.credential;
+  }
+
+
+  /**
+   * This returns the filter used to authorize users.
+   *
+   * @return  <code>String</code> - filter
+   */
+  public String getAuthorizationFilter()
+  {
+    return this.authorizationFilter;
+  }
+
+
+  /**
+   * This returns the filter arguments used to authorize users.
+   *
+   * @return  <code>Object[]</code> - filter arguments
+   */
+  public Object[] getAuthorizationFilterArgs()
+  {
+    return this.authorizationFilterArgs;
+  }
+
+
+  /**
+   * This returns the constructDn of the <code>Authenticator</code>.
+   *
+   * @return  <code>boolean</code> - whether the DN will be constructed
+   */
+  public boolean getConstructDn()
+  {
+    return
+      this.dnResolver != null &&
+        this.dnResolver.getClass().isAssignableFrom(ConstructDnResolver.class);
+  }
+
+
+  /**
+   * This returns the allowMultipleDns of the <code>Authenticator</code>.
+   *
+   * @return  <code>boolean</code> - whether an exception will be thrown if
+   * multiple DNs are found
+   */
+  public boolean getAllowMultipleDns()
+  {
+    return this.allowMultipleDns;
+  }
+
+
+  /**
+   * This returns the subtreeSearch of the <code>Authenticator</code>.
+   *
+   * @return  <code>boolean</code> - whether the DN will be searched for over
+   * the entire base
+   */
+  public boolean getSubtreeSearch()
+  {
+    return SearchScope.SUBTREE == this.getSearchScope();
+  }
+
+
+  /**
+   * This returns the DN resolver.
+   *
+   * @return  <code>DnResolver</code>
+   */
+  public DnResolver getDnResolver()
+  {
+    return this.dnResolver;
+  }
+
+
+  /**
+   * This returns the authentication handler.
+   *
+   * @return  <code>AuthenticationHandler</code>
+   */
+  public AuthenticationHandler getAuthenticationHandler()
+  {
+    return this.authenticationHandler;
+  }
+
+
+  /**
+   * This returns the handlers to use for processing authentications.
+   *
+   * @return  <code>AuthenticationResultHandler[]</code>
+   */
+  public AuthenticationResultHandler[] getAuthenticationResultHandlers()
+  {
+    return this.authenticationResultHandlers;
+  }
+
+
+  /**
+   * This returns the handlers to use for processing authorization.
+   *
+   * @return  <code>AuthorizationHandler[]</code>
+   */
+  public AuthorizationHandler[] getAuthorizationHandlers()
+  {
+    return this.authorizationHandlers;
+  }
+
+
+  /**
+   * This sets the user fields for the <code>Authenticator</code>. The user
+   * field is used to lookup a user's dn.
+   *
+   * @param  userField  <code>String[]</code> username
+   */
+  public void setUserField(final String[] userField)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting userField: " + Arrays.toString(userField));
+    }
+    this.userField = userField;
+  }
+
+
+  /**
+   * This sets the filter used to search for users. If not set, the user field
+   * is used to build a search filter.
+   *
+   * @param  userFilter  <code>String</code>
+   */
+  public void setUserFilter(final String userFilter)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting userFilter: " + userFilter);
+    }
+    this.userFilter = userFilter;
+  }
+
+
+  /**
+   * This sets the filter arguments used to search for users.
+   *
+   * @param  userFilterArgs  <code>Object[]</code>
+   */
+  public void setUserFilterArgs(final Object[] userFilterArgs)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "setting userFilterArgs: " + Arrays.toString(userFilterArgs));
+    }
+    this.userFilterArgs = userFilterArgs;
+  }
+
+
+  /**
+   * This sets the username for the <code>Authenticator</code> to use for
+   * authentication.
+   *
+   * @param  user  <code>String</code> username
+   */
+  public void setUser(final String user)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting user: " + user);
+    }
+    this.user = user;
+  }
+
+  /**
+   * This sets the credential for the <code>Authenticator</code> to use for
+   * authentication.
+   *
+   * @param  credential  <code>Object</code>
+   */
+  public void setCredential(final Object credential)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      if (this.getLogCredentials()) {
+        this.logger.trace("setting credential: " + credential);
+      } else {
+        this.logger.trace("setting credential: <suppressed>");
+      }
+    }
+    this.credential = credential;
+  }
+
+
+  /**
+   * This sets the filter used to authorize users. If not set, no authorization
+   * is performed.
+   *
+   * @param  authorizationFilter  <code>String</code>
+   */
+  public void setAuthorizationFilter(final String authorizationFilter)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting authorizationFilter: " + authorizationFilter);
+    }
+    this.authorizationFilter = authorizationFilter;
+  }
+
+
+  /**
+   * This sets the filter arguments used to authorize users.
+   *
+   * @param  authorizationFilterArgs  <code>Object[]</code>
+   */
+  public void setAuthorizationFilterArgs(final Object[] authorizationFilterArgs)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "setting authorizationFilterArgs: " +
+        Arrays.toString(authorizationFilterArgs));
+    }
+    this.authorizationFilterArgs = authorizationFilterArgs;
+  }
+
+
+  /**
+   * This sets the constructDn for the <code>Authenticator</code>. If true, the
+   * {@link #dnResolver} is set to {@link ConstructDnResolver}. If false, the
+   * {@link #dnResolver} is set to {@link SearchDnResolver}.
+   *
+   * @param  constructDn  <code>boolean</code>
+   */
+  public void setConstructDn(final boolean constructDn)
+  {
+    if (constructDn) {
+      this.setDnResolver(new ConstructDnResolver());
+    } else {
+      this.setDnResolver(new SearchDnResolver());
+    }
+  }
+
+
+  /**
+   * This sets the allowMultipleDns for the <code>Authentication</code>. If
+   * false an exception will be thrown if {@link Authenticator#getDn(String)}
+   * finds more than one DN matching it's filter. Otherwise the first DN found
+   * is returned.
+   *
+   * @param  allowMultipleDns  <code>boolean</code>
+   */
+  public void setAllowMultipleDns(final boolean allowMultipleDns)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting allowMultipleDns: " + allowMultipleDns);
+    }
+    this.allowMultipleDns = allowMultipleDns;
+  }
+
+
+  /**
+   * This sets the subtreeSearch for the <code>Authenticator</code>. If true,
+   * the DN used for authenticating will be searched for over the entire {@link
+   * LdapConfig#getBaseDn()}. Otherwise the DN will be search for in the {@link
+   * LdapConfig#getBaseDn()} context.
+   *
+   * @param  subtreeSearch  <code>boolean</code>
+   */
+  public void setSubtreeSearch(final boolean subtreeSearch)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting subtreeSearch: " + subtreeSearch);
+    }
+    if (subtreeSearch) {
+      this.setSearchScope(SearchScope.SUBTREE);
+    } else {
+      this.setSearchScope(SearchScope.ONELEVEL);
+    }
+  }
+
+
+  /**
+   * This sets the DN resolver.
+   *
+   * @param  resolver  <code>DnResolver</code>
+   */
+  public void setDnResolver(final DnResolver resolver)
+  {
+
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting dnResolver: " + resolver);
+    }
+    this.dnResolver = resolver;
+    if (this.dnResolver != null) {
+      this.dnResolver.setAuthenticatorConfig(this);
+    }
+  }
+
+
+  /**
+   * This sets the authentication handler.
+   *
+   * @param  handler  <code>AuthenticationHandler</code>
+   */
+  public void setAuthenticationHandler(final AuthenticationHandler handler)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting authenticationHandler: " + handler);
+    }
+    this.authenticationHandler = handler;
+    if (this.authenticationHandler != null) {
+      this.authenticationHandler.setAuthenticatorConfig(this);
+    }
+  }
+
+
+  /**
+   * This sets the handlers for processing authentications.
+   *
+   * @param  handlers  <code>AuthenticationResultHandler[]</code>
+   */
+  public void setAuthenticationResultHandlers(
+    final AuthenticationResultHandler[] handlers)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting authenticationResultHandlers: " + handlers);
+    }
+    this.authenticationResultHandlers = handlers;
+  }
+
+
+  /**
+   * This sets the handlers for processing authorization.
+   *
+   * @param  handlers  <code>AuthorizationHandler[]</code>
+   */
+  public void setAuthorizationHandlers(final AuthorizationHandler[] handlers)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting authorizationHandlers: " + handlers);
+    }
+    this.authorizationHandlers = handlers;
+  }
+
+
+  /** {@inheritDoc} */
+  public String getPropertiesDomain()
+  {
+    return PROPERTIES_DOMAIN;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setEnvironmentProperties(final String name, final String value)
+  {
+    checkImmutable();
+    if (name != null && value != null) {
+      if (PROPERTIES.hasProperty(name)) {
+        PROPERTIES.setProperty(this, name, value);
+      } else {
+        super.setEnvironmentProperties(name, value);
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean hasEnvironmentProperty(final String name)
+  {
+    return PROPERTIES.hasProperty(name);
+  }
+
+
+  /**
+   * Create an instance of this class initialized with properties from the input
+   * stream. If the input stream is null, load properties from the default
+   * properties file.
+   *
+   * @param  is  to load properties from
+   *
+   * @return  <code>AuthenticatorConfig</code> initialized ldap pool config
+   */
+  public static AuthenticatorConfig createFromProperties(final InputStream is)
+  {
+    final AuthenticatorConfig authConfig = new AuthenticatorConfig();
+    LdapProperties properties = null;
+    if (is != null) {
+      properties = new LdapProperties(authConfig, is);
+    } else {
+      properties = new LdapProperties(authConfig);
+      properties.useDefaultPropertiesFile();
+    }
+    properties.configure();
+    return authConfig;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/AuthorizationException.java b/src/main/java/edu/vt/middleware/ldap/auth/AuthorizationException.java
new file mode 100644
index 0000000..d0cf3a6
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/AuthorizationException.java
@@ -0,0 +1,49 @@
+/*
+  $Id: AuthorizationException.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import javax.naming.NamingException;
+
+/**
+ * <code>AuthorizationException</code> is thrown when an attempt to authorize a
+ * user fails.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class AuthorizationException extends NamingException
+{
+
+  /** serialVersionUID. */
+  private static final long serialVersionUID = -6290236661997869406L;
+
+
+  /** Default constructor. */
+  public AuthorizationException()
+  {
+    super();
+  }
+
+
+  /**
+   * This creates a new <code>AuthorizationException</code> with the supplied
+   * <code>String</code>.
+   *
+   * @param  msg  <code>String</code>
+   */
+  public AuthorizationException(final String msg)
+  {
+    super(msg);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/ConstructDnResolver.java b/src/main/java/edu/vt/middleware/ldap/auth/ConstructDnResolver.java
new file mode 100644
index 0000000..241f0de
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/ConstructDnResolver.java
@@ -0,0 +1,114 @@
+/*
+  $Id: ConstructDnResolver.java 1632 2010-09-28 22:42:24Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1632 $
+  Updated: $Date: 2010-09-28 23:42:24 +0100 (Tue, 28 Sep 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.io.Serializable;
+import javax.naming.NamingException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>ConstructDnResolver</code> creates an LDAP DN using known information
+ * about the LDAP. Specifically it concatenates the first user field with the
+ * base DN.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1632 $ $Date: 2010-09-28 23:42:24 +0100 (Tue, 28 Sep 2010) $
+ */
+public class ConstructDnResolver implements DnResolver, Serializable
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -6508789359608064771L;
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Authentication configuration. */
+  protected AuthenticatorConfig config;
+
+
+  /** Default constructor. */
+  public ConstructDnResolver() {}
+
+
+  /**
+   * This will create a new <code>ConstructDnResolver</code> with the supplied
+   * <code>AuthenticatorConfig</code>.
+   *
+   * @param  authConfig  <code>AuthenticatorConfig</code>
+   */
+  public ConstructDnResolver(final AuthenticatorConfig authConfig)
+  {
+    this.setAuthenticatorConfig(authConfig);
+  }
+
+
+  /**
+   * This will set the config parameters of this <code>Authenticator</code>.
+   *
+   * @param  authConfig  <code>AuthenticatorConfig</code>
+   */
+  public void setAuthenticatorConfig(final AuthenticatorConfig authConfig)
+  {
+    this.config = authConfig;
+  }
+
+
+  /**
+   * This returns the <code>AuthenticatorConfig</code> of the <code>
+   * Authenticator</code>.
+   *
+   * @return  <code>AuthenticatorConfig</code>
+   */
+  public AuthenticatorConfig getAuthenticatorConfig()
+  {
+    return this.config;
+  }
+
+
+  /**
+   * Creates a LDAP DN by combining the userField and the base dn.
+   *
+   * @param  user  <code>String</code> to find dn for
+   *
+   * @return  <code>String</code> - user's dn
+   *
+   * @throws  NamingException  if the LDAP search fails
+   */
+  public String resolve(final String user)
+    throws NamingException
+  {
+    String dn = null;
+    if (user != null && !"".equals(user)) {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Constructing DN from first userfield and base");
+      }
+      dn = String.format(
+        "%s=%s,%s",
+        this.config.getUserField()[0],
+        user,
+        this.config.getBaseDn());
+    } else {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("User input was empty or null");
+      }
+    }
+    return dn;
+  }
+
+
+  /** {@inheritDoc} */
+  public void close() {}
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/DnResolver.java b/src/main/java/edu/vt/middleware/ldap/auth/DnResolver.java
new file mode 100644
index 0000000..f5b188f
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/DnResolver.java
@@ -0,0 +1,58 @@
+/*
+  $Id: DnResolver.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import javax.naming.NamingException;
+
+/**
+ * <code>DnResolver</code> provides an interface for finding LDAP DNs.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface DnResolver
+{
+
+  /**
+   * Attempts to find the LDAP DN for the supplied user.
+   *
+   * @param  user  <code>String</code> to find dn for
+   *
+   * @return  <code>String</code> - user's dn
+   *
+   * @throws  NamingException  if an LDAP error occurs
+   */
+  String resolve(String user)
+    throws NamingException;
+
+
+  /**
+   * Returns the authenticator config.
+   *
+   * @return  authenticator configuration
+   */
+  AuthenticatorConfig getAuthenticatorConfig();
+
+
+  /**
+   * Sets the authenticator config.
+   *
+   * @param  config  authenticator configuration
+   */
+  void setAuthenticatorConfig(AuthenticatorConfig config);
+
+
+  /** This will close any resources associated with this resolver. */
+  void close();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/NoopDnResolver.java b/src/main/java/edu/vt/middleware/ldap/auth/NoopDnResolver.java
new file mode 100644
index 0000000..3fd6a01
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/NoopDnResolver.java
@@ -0,0 +1,73 @@
+/*
+  $Id: NoopDnResolver.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.io.Serializable;
+import javax.naming.NamingException;
+
+/**
+ * <code>NoopDnResolver</code> returns the user as the LDAP DN.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class NoopDnResolver implements DnResolver, Serializable
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -7832850056696716639L;
+
+
+  /** Default constructor. */
+  public NoopDnResolver() {}
+
+
+  /**
+   * This method is not implemented.
+   *
+   * @param  authConfig  <code>AuthenticatorConfig</code>
+   */
+  public void setAuthenticatorConfig(final AuthenticatorConfig authConfig) {}
+
+
+  /**
+   * This method is not implemented.
+   *
+   * @return  null
+   */
+  public AuthenticatorConfig getAuthenticatorConfig()
+  {
+    return null;
+  }
+
+
+  /**
+   * Returns the user as the LDAP DN.
+   *
+   * @param  user  <code>String</code> to find dn for
+   *
+   * @return  <code>String</code> - user's dn
+   *
+   * @throws  NamingException  if the LDAP search fails
+   */
+  public String resolve(final String user)
+    throws NamingException
+  {
+    return user;
+  }
+
+
+  /** {@inheritDoc} */
+  public void close() {}
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/SearchDnResolver.java b/src/main/java/edu/vt/middleware/ldap/auth/SearchDnResolver.java
new file mode 100644
index 0000000..81eef35
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/SearchDnResolver.java
@@ -0,0 +1,185 @@
+/*
+  $Id: SearchDnResolver.java 1634 2010-09-29 20:03:09Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1634 $
+  Updated: $Date: 2010-09-29 21:03:09 +0100 (Wed, 29 Sep 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.AbstractLdap;
+import edu.vt.middleware.ldap.SearchFilter;
+
+/**
+ * <code>SearchDnResolver</code> looks up a user's DN using an LDAP search.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1634 $ $Date: 2010-09-29 21:03:09 +0100 (Wed, 29 Sep 2010) $
+ */
+public class SearchDnResolver extends AbstractLdap<AuthenticatorConfig>
+  implements DnResolver, Serializable
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -7615995272176088807L;
+
+
+  /** Default constructor. */
+  public SearchDnResolver() {}
+
+
+  /**
+   * This will create a new <code>SearchDnResolver</code> with the supplied
+   * <code>AuthenticatorConfig</code>.
+   *
+   * @param  authConfig  <code>AuthenticatorConfig</code>
+   */
+  public SearchDnResolver(final AuthenticatorConfig authConfig)
+  {
+    this.setAuthenticatorConfig(authConfig);
+  }
+
+
+  /**
+   * This will set the config parameters of this <code>Authenticator</code>.
+   *
+   * @param  authConfig  <code>AuthenticatorConfig</code>
+   */
+  public void setAuthenticatorConfig(final AuthenticatorConfig authConfig)
+  {
+    super.setLdapConfig(authConfig);
+  }
+
+
+  /**
+   * This returns the <code>AuthenticatorConfig</code> of the <code>
+   * Authenticator</code>.
+   *
+   * @return  <code>AuthenticatorConfig</code>
+   */
+  public AuthenticatorConfig getAuthenticatorConfig()
+  {
+    return this.config;
+  }
+
+
+  /**
+   * This will attempt to find the dn for the supplied user. {@link
+   * AuthenticatorConfig#getUserFilter()} or {@link
+   * AuthenticatorConfig#getUserField()} is used to look up the dn. If a filter
+   * is used, the user is provided as the {0} variable filter argument. If a
+   * field is used, the filter is built by ORing the fields together. If more
+   * than one entry matches the search, the result is controlled by {@link
+   * AuthenticatorConfig#setAllowMultipleDns(boolean)}.
+   *
+   * @param  user  <code>String</code> to find dn for
+   *
+   * @return  <code>String</code> - user's dn
+   *
+   * @throws  NamingException  if the LDAP search fails
+   */
+  public String resolve(final String user)
+    throws NamingException
+  {
+    String dn = null;
+    if (user != null && !"".equals(user)) {
+      // create the search filter
+      final SearchFilter filter = new SearchFilter();
+      if (this.config.getUserFilter() != null) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Looking up DN using userFilter");
+        }
+        filter.setFilter(this.config.getUserFilter());
+        filter.setFilterArgs(this.config.getUserFilterArgs());
+      } else {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Looking up DN using userField");
+        }
+        if (
+          this.config.getUserField() == null ||
+            this.config.getUserField().length == 0) {
+          if (this.logger.isErrorEnabled()) {
+            this.logger.error("Invalid userField, cannot be null or empty.");
+          }
+        } else {
+          final StringBuffer searchFilter = new StringBuffer();
+          if (this.config.getUserField().length > 1) {
+            searchFilter.append("(|");
+            for (int i = 0; i < this.config.getUserField().length; i++) {
+              searchFilter.append("(").append(this.config.getUserField()[i])
+                .append("={0})");
+            }
+            searchFilter.append(")");
+          } else {
+            searchFilter.append("(").append(this.config.getUserField()[0])
+              .append("={0})");
+          }
+          filter.setFilter(searchFilter.toString());
+        }
+      }
+
+      if (filter.getFilter() != null) {
+        // make user the first filter arg
+        final List<Object> filterArgs = new ArrayList<Object>();
+        filterArgs.add(user);
+        filterArgs.addAll(filter.getFilterArgs());
+
+        final Iterator<SearchResult> answer = this.search(
+          this.config.getBaseDn(),
+          filter.getFilter(),
+          filterArgs.toArray(),
+          this.config.getSearchControls(new String[0]),
+          this.config.getSearchResultHandlers());
+        // return first match, otherwise user doesn't exist
+        if (answer != null && answer.hasNext()) {
+          final SearchResult sr = answer.next();
+          dn = sr.getName();
+          if (answer.hasNext()) {
+            if (this.logger.isDebugEnabled()) {
+              this.logger.debug(
+                "Multiple results found for user: " + user + " using filter: " +
+                filter);
+            }
+            if (!this.config.getAllowMultipleDns()) {
+              throw new NamingException("Found more than (1) DN for: " + user);
+            }
+          }
+        } else {
+          if (this.logger.isInfoEnabled()) {
+            this.logger.info(
+              "Search for user: " + user + " failed using filter: " +
+              filter.getFilter());
+          }
+        }
+      } else {
+        if (this.logger.isErrorEnabled()) {
+          this.logger.error("DN search filter not found, no search performed");
+        }
+      }
+    } else {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("User input was empty or null");
+      }
+    }
+    return dn;
+  }
+
+
+  /** {@inheritDoc} */
+  public void close()
+  {
+    super.close();
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/handler/AbstractAuthenticationHandler.java b/src/main/java/edu/vt/middleware/ldap/auth/handler/AbstractAuthenticationHandler.java
new file mode 100644
index 0000000..514e867
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/handler/AbstractAuthenticationHandler.java
@@ -0,0 +1,56 @@
+/*
+  $Id: AbstractAuthenticationHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.auth.AuthenticatorConfig;
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * AbstractAuthenticationHandler provides a base implementation for
+ * authentication handlers.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public abstract class AbstractAuthenticationHandler
+  implements AuthenticationHandler
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Authenticator configuration. */
+  protected AuthenticatorConfig config;
+
+
+  /** {@inheritDoc} */
+  public void setAuthenticatorConfig(final AuthenticatorConfig ac)
+  {
+    this.config = ac;
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract void authenticate(
+    final ConnectionHandler ch,
+    final AuthenticationCriteria ac)
+    throws NamingException;
+
+
+  /** {@inheritDoc} */
+  public abstract AuthenticationHandler newInstance();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationCriteria.java b/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationCriteria.java
new file mode 100644
index 0000000..5241ee7
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationCriteria.java
@@ -0,0 +1,102 @@
+/*
+  $Id: AuthenticationCriteria.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+/**
+ * <code>AuthenticationCriteria</code> contains the attributes used to perform
+ * authentications.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class AuthenticationCriteria
+{
+
+  /** dn. */
+  private String dn;
+
+  /** credential. */
+  private Object credential;
+
+
+  /** Default constructor. */
+  public AuthenticationCriteria() {}
+
+
+  /**
+   * Creates a new authentication criteria with the supplied dn.
+   *
+   * @param  s  to set dn
+   */
+  public AuthenticationCriteria(final String s)
+  {
+    this.dn = s;
+  }
+
+
+  /**
+   * Gets the dn.
+   *
+   * @return  dn
+   */
+  public String getDn()
+  {
+    return this.dn;
+  }
+
+
+  /**
+   * Sets the dn.
+   *
+   * @param  s  to set dn
+   */
+  public void setDn(final String s)
+  {
+    this.dn = s;
+  }
+
+
+  /**
+   * Gets the credential.
+   *
+   * @return  credential
+   */
+  public Object getCredential()
+  {
+    return this.credential;
+  }
+
+
+  /**
+   * Sets the credential.
+   *
+   * @param  o  to set credential
+   */
+  public void setCredential(final Object o)
+  {
+    this.credential = o;
+  }
+
+
+  /**
+   * This returns a string representation of this search criteria.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return String.format("dn=%s,credential=%s", this.dn, this.credential);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationHandler.java b/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationHandler.java
new file mode 100644
index 0000000..8286c61
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationHandler.java
@@ -0,0 +1,62 @@
+/*
+  $Id: AuthenticationHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.auth.AuthenticatorConfig;
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+
+/**
+ * <code>AuthenticationHandler</code> provides an interface for LDAP
+ * authentication implementations.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public interface AuthenticationHandler
+{
+
+
+  /**
+   * Sets the authenticator configuration.
+   *
+   * @param  ac  authenticator config
+   */
+  void setAuthenticatorConfig(AuthenticatorConfig ac);
+
+
+  /**
+   * Perform an ldap authentication. Implementations should throw <code>
+   * AuthenticationException</code> to indicate an authentication failure. The
+   * resulting <code>LdapContext</code> can be retrieved from the connection
+   * handler if it is needed.
+   *
+   * @param  ch  <code>ConnectionHandler</code> to communicate with the LDAP
+   * @param  ac  <code>AuthenticationCriteria</code> to perform the
+   * authentication with
+   *
+   * @throws  AuthenticationException  if authentication fails
+   * @throws  NamingException  if an LDAP error occurs
+   */
+  void authenticate(ConnectionHandler ch, AuthenticationCriteria ac)
+    throws NamingException;
+
+
+  /**
+   * Returns a separate instance of this authentication handler.
+   *
+   * @return  authentication handler
+   */
+  AuthenticationHandler newInstance();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationResultHandler.java b/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationResultHandler.java
new file mode 100644
index 0000000..11ee9d6
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthenticationResultHandler.java
@@ -0,0 +1,35 @@
+/*
+  $Id: AuthenticationResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+/**
+ * AuthenticationResultHandler provides post processing of authentication
+ * results.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public interface AuthenticationResultHandler
+{
+
+
+  /**
+   * Process the results from an ldap authentication.
+   *
+   * @param  ac  <code>AuthenticationCriteria</code> used to perform the
+   * authentication
+   * @param  success  <code>boolean</code> whether the authentication succeeded
+   */
+  void process(AuthenticationCriteria ac, boolean success);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthorizationHandler.java b/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthorizationHandler.java
new file mode 100644
index 0000000..bb49179
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/handler/AuthorizationHandler.java
@@ -0,0 +1,46 @@
+/*
+  $Id: AuthorizationHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+
+/**
+ * AuthorizationHandler provides processing of authorization queries after
+ * authentication has succeeded.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public interface AuthorizationHandler
+{
+
+
+  /**
+   * Process an authorization after an ldap authentication. The supplied
+   * LdapContext should <b>not</b> be closed in this method. Implementations
+   * should throw <code>AuthorizationException</code> to indicate an
+   * authorization failure.
+   *
+   * @param  ac  <code>AuthenticationCriteria</code> used to perform the
+   * authentication
+   * @param  ctx  <code>LdapContext</code> authenticated context used to perform
+   * the bind
+   *
+   * @throws  AuthorizationException  if authorization fails
+   * @throws  NamingException  if an LDAP error occurs
+   */
+  void process(AuthenticationCriteria ac, LdapContext ctx)
+    throws NamingException;
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/handler/BindAuthenticationHandler.java b/src/main/java/edu/vt/middleware/ldap/auth/handler/BindAuthenticationHandler.java
new file mode 100644
index 0000000..8540abb
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/handler/BindAuthenticationHandler.java
@@ -0,0 +1,62 @@
+/*
+  $Id: BindAuthenticationHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.auth.AuthenticatorConfig;
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+
+/**
+ * <code>BindAuthenticationHandler</code> provides an LDAP authentication
+ * implementation that leverages the LDAP bind operation.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class BindAuthenticationHandler extends AbstractAuthenticationHandler
+{
+
+
+  /** Default constructor. */
+  public BindAuthenticationHandler() {}
+
+
+  /**
+   * Creates a new <code>BindAuthenticationHandler</code> with the supplied
+   * authenticator config.
+   *
+   * @param  ac  authenticator config
+   */
+  public BindAuthenticationHandler(final AuthenticatorConfig ac)
+  {
+    this.setAuthenticatorConfig(ac);
+  }
+
+
+  /** {@inheritDoc} */
+  public void authenticate(
+    final ConnectionHandler ch,
+    final AuthenticationCriteria ac)
+    throws NamingException
+  {
+    ch.connect(ac.getDn(), ac.getCredential());
+  }
+
+
+  /** {@inheritDoc} */
+  public BindAuthenticationHandler newInstance()
+  {
+    return new BindAuthenticationHandler(this.config);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/handler/CompareAuthenticationHandler.java b/src/main/java/edu/vt/middleware/ldap/auth/handler/CompareAuthenticationHandler.java
new file mode 100644
index 0000000..50e1129
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/handler/CompareAuthenticationHandler.java
@@ -0,0 +1,128 @@
+/*
+  $Id: CompareAuthenticationHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import javax.naming.AuthenticationException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.LdapUtil;
+import edu.vt.middleware.ldap.auth.AuthenticatorConfig;
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+
+/**
+ * <code>CompareAuthenticationHandler</code> provides an LDAP authentication
+ * implementation that leverages a compare operation against the userPassword
+ * attribute. The default password scheme used is 'SHA'.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class CompareAuthenticationHandler extends AbstractAuthenticationHandler
+{
+
+  /** Maximum digest size. Value is {@value}. */
+  private static final int DIGEST_SIZE = 256;
+
+  /** Password scheme. Default value is {@value}. */
+  private String passwordScheme = "SHA";
+
+
+  /** Default constructor. */
+  public CompareAuthenticationHandler() {}
+
+
+  /**
+   * Creates a new <code>CompareAuthenticationHandler</code> with the supplied
+   * authenticator config.
+   *
+   * @param  ac  authenticator config
+   */
+  public CompareAuthenticationHandler(final AuthenticatorConfig ac)
+  {
+    this.setAuthenticatorConfig(ac);
+  }
+
+
+  /**
+   * Returns the password scheme.
+   *
+   * @return  password scheme
+   */
+  public String getPasswordScheme()
+  {
+    return this.passwordScheme;
+  }
+
+
+  /**
+   * Sets the password scheme. Must equal a known message digest algorithm.
+   *
+   * @param  s  password scheme
+   */
+  public void setPasswordScheme(final String s)
+  {
+    this.passwordScheme = s;
+  }
+
+
+  /** {@inheritDoc} */
+  public void authenticate(
+    final ConnectionHandler ch,
+    final AuthenticationCriteria ac)
+    throws NamingException
+  {
+    byte[] hash = new byte[DIGEST_SIZE];
+    try {
+      final MessageDigest md = MessageDigest.getInstance(this.passwordScheme);
+      md.update(((String) ac.getCredential()).getBytes());
+      hash = md.digest();
+    } catch (NoSuchAlgorithmException e) {
+      throw new NamingException(e.getMessage());
+    }
+
+    ch.connect(this.config.getBindDn(), this.config.getBindCredential());
+
+    NamingEnumeration<SearchResult> en = null;
+    try {
+      en = ch.getLdapContext().search(
+        ac.getDn(),
+        "userPassword={0}",
+        new Object[] {
+          String.format(
+            "{%s}%s",
+            this.passwordScheme,
+            LdapUtil.base64Encode(hash)).getBytes(),
+        },
+        LdapConfig.getCompareSearchControls());
+      if (!en.hasMore()) {
+        throw new AuthenticationException("Compare authentication failed.");
+      }
+    } finally {
+      if (en != null) {
+        en.close();
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public CompareAuthenticationHandler newInstance()
+  {
+    return new CompareAuthenticationHandler(this.config);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/auth/handler/CompareAuthorizationHandler.java b/src/main/java/edu/vt/middleware/ldap/auth/handler/CompareAuthorizationHandler.java
new file mode 100644
index 0000000..e9da2b3
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/auth/handler/CompareAuthorizationHandler.java
@@ -0,0 +1,124 @@
+/*
+  $Id: CompareAuthorizationHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.LdapContext;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.auth.AuthorizationException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * CompareAuthorizationHandler performs a compare operation with a custom
+ * filter. The DN of the authenticated user is automatically provided as the {0}
+ * variable in the search filter arguments.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class CompareAuthorizationHandler implements AuthorizationHandler
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+
+  /** Search filter. */
+  private SearchFilter searchFilter;
+
+
+  /** Default constructor. */
+  public CompareAuthorizationHandler() {}
+
+
+  /**
+   * Creates a new <code>CompareAuthorizationHandler</code> with the supplied
+   * search filter.
+   *
+   * @param  sf  <code>SearchFilter</code>
+   */
+  public CompareAuthorizationHandler(final SearchFilter sf)
+  {
+    this.searchFilter = sf;
+  }
+
+
+  /**
+   * Returns the search filter.
+   *
+   * @return  <code>SearchFilter</code>
+   */
+  public SearchFilter getSearchFilter()
+  {
+    return this.searchFilter;
+  }
+
+
+  /**
+   * Sets the search filter.
+   *
+   * @param  sf  <code>SearchFilter</code>
+   */
+  public void setSearchFilter(final SearchFilter sf)
+  {
+    this.searchFilter = sf;
+  }
+
+
+  /** {@inheritDoc} */
+  public void process(final AuthenticationCriteria ac, final LdapContext ctx)
+    throws NamingException
+  {
+    // make DN the first filter arg
+    final List<Object> filterArgs = new ArrayList<Object>();
+    filterArgs.add(ac.getDn());
+    filterArgs.addAll(this.searchFilter.getFilterArgs());
+
+    // perform ldap compare operation
+    NamingEnumeration<SearchResult> results = null;
+    try {
+      results = ctx.search(
+        ac.getDn(),
+        this.searchFilter.getFilter(),
+        filterArgs.toArray(),
+        LdapConfig.getCompareSearchControls());
+      if (!results.hasMore()) {
+        throw new AuthorizationException("Compare failed");
+      }
+    } finally {
+      if (results != null) {
+        results.close();
+      }
+    }
+  }
+
+
+  /**
+   * Provides a descriptive string representation of this authorization handler.
+   *
+   * @return  String of the form $Classname::$filter.
+   */
+  @Override
+  public String toString()
+  {
+    return
+      String.format("%s::%s", this.getClass().getName(), this.searchFilter);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapAttribute.java b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapAttribute.java
new file mode 100644
index 0000000..1acb8c1
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapAttribute.java
@@ -0,0 +1,160 @@
+/*
+  $Id: AbstractLdapAttribute.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.Set;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.BasicAttribute;
+import edu.vt.middleware.ldap.LdapUtil;
+
+/**
+ * <code>AbstractLdapAttribute</code> provides a base implementation of <code>
+ * LdapAttribute</code> where the underlying values are backed by a <code>
+ * Set</code>.
+ *
+ * @param  <T>  type of backing set
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class AbstractLdapAttribute<T extends Set<Object>>
+  extends AbstractLdapBean implements LdapAttribute
+{
+
+  /** hash code seed. */
+  protected static final int HASH_CODE_SEED = 41;
+
+  /** Name for this attribute. */
+  protected String name;
+
+  /** Values for this attribute. */
+  protected Set<Object> values;
+
+
+  /**
+   * Creates a new <code>AbstractLdapAttribute</code> with the supplied ldap
+   * bean factory.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public AbstractLdapAttribute(final LdapBeanFactory lbf)
+  {
+    super(lbf);
+  }
+
+
+  /** {@inheritDoc} */
+  public String getName()
+  {
+    return this.name;
+  }
+
+
+  /** {@inheritDoc} */
+  public Set<Object> getValues()
+  {
+    return this.values;
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract Set<String> getStringValues();
+
+
+  /** {@inheritDoc} */
+  public void setAttribute(final Attribute attribute)
+    throws NamingException
+  {
+    this.setName(attribute.getID());
+
+    final NamingEnumeration<?> ne = attribute.getAll();
+    while (ne.hasMore()) {
+      this.values.add(ne.next());
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void setName(final String name)
+  {
+    this.name = name;
+  }
+
+
+  /** {@inheritDoc} */
+  public int hashCode()
+  {
+    int hc = HASH_CODE_SEED;
+    if (this.name != null) {
+      hc += this.name.hashCode();
+    }
+    for (String s : this.getStringValues()) {
+      if (s != null) {
+        hc += s.hashCode();
+      }
+    }
+    return hc;
+  }
+
+
+  /**
+   * This returns a string representation of this object.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return String.format("%s%s", this.name, this.values);
+  }
+
+
+  /** {@inheritDoc} */
+  public Attribute toAttribute()
+  {
+    final Attribute attribute = new BasicAttribute(this.name);
+    for (Object o : this.values) {
+      attribute.add(o);
+    }
+    return attribute;
+  }
+
+
+  /**
+   * Converts the underlying set of objects to a set of strings. Objects of type
+   * byte[] are base64 encoded. Objects which are not of type String or byte[]
+   * are converted using Object.toString().
+   *
+   * @param  stringValues  <code>Set</code> to populate with strings
+   */
+  protected void convertValuesToString(final Set<String> stringValues)
+  {
+    for (Object o : this.values) {
+      if (o != null) {
+        if (o instanceof String) {
+          stringValues.add((String) o);
+        } else if (o instanceof byte[]) {
+          final String encodedValue = LdapUtil.base64Encode((byte[]) o);
+          if (encodedValue != null) {
+            stringValues.add(encodedValue);
+          }
+        } else {
+          stringValues.add(o.toString());
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapAttributes.java b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapAttributes.java
new file mode 100644
index 0000000..ba967b0
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapAttributes.java
@@ -0,0 +1,216 @@
+/*
+  $Id: AbstractLdapAttributes.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+
+/**
+ * <code>AbstractLdapAttributes</code> provides a base implementation of <code>
+ * LdapAttributes</code> where the underlying attributes are backed by a <code>
+ * Map</code>.
+ *
+ * @param  <T>  type of backing map
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class
+AbstractLdapAttributes<T extends Map<String, LdapAttribute>>
+  extends AbstractLdapBean implements LdapAttributes
+{
+
+  /** Whether to ignore case when creating <code>BasicAttributes</code>. */
+  public static final boolean DEFAULT_IGNORE_CASE = true;
+
+  /** hash code seed. */
+  protected static final int HASH_CODE_SEED = 42;
+
+  /** Attributes contained in this bean. */
+  protected T attributes;
+
+
+  /**
+   * Creates a new <code>AbstractLdapAttributes</code> with the supplied ldap
+   * bean factory.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public AbstractLdapAttributes(final LdapBeanFactory lbf)
+  {
+    super(lbf);
+  }
+
+
+  /** {@inheritDoc} */
+  public Collection<LdapAttribute> getAttributes()
+  {
+    return this.attributes.values();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapAttribute getAttribute(final String name)
+  {
+    return this.attributes.get(name);
+  }
+
+
+  /** {@inheritDoc} */
+  public String[] getAttributeNames()
+  {
+    return this.attributes.keySet().toArray(new String[0]);
+  }
+
+
+  /** {@inheritDoc} */
+  public void addAttribute(final LdapAttribute a)
+  {
+    this.attributes.put(a.getName(), a);
+  }
+
+
+  /** {@inheritDoc} */
+  public void addAttribute(final String name, final Object value)
+  {
+    final LdapAttribute la = this.beanFactory.newLdapAttribute();
+    la.setName(name);
+    la.getValues().add(value);
+    this.addAttribute(la);
+  }
+
+
+  /** {@inheritDoc} */
+  public void addAttribute(final String name, final List<?> values)
+  {
+    final LdapAttribute la = this.beanFactory.newLdapAttribute();
+    la.setName(name);
+    la.getValues().addAll(values);
+    this.addAttribute(la);
+  }
+
+
+  /** {@inheritDoc} */
+  public void addAttributes(final Collection<LdapAttribute> c)
+  {
+    for (LdapAttribute la : c) {
+      this.addAttribute(la);
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void addAttributes(final Attributes a)
+    throws NamingException
+  {
+    final NamingEnumeration<? extends Attribute> ne = a.getAll();
+    while (ne.hasMore()) {
+      final LdapAttribute la = this.beanFactory.newLdapAttribute();
+      la.setAttribute(ne.next());
+      this.addAttribute(la);
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void removeAttribute(final LdapAttribute a)
+  {
+    this.attributes.remove(a.getName());
+  }
+
+
+  /** {@inheritDoc} */
+  public void removeAttribute(final String name)
+  {
+    this.attributes.remove(name);
+  }
+
+
+  /** {@inheritDoc} */
+  public void removeAttributes(final Collection<LdapAttribute> c)
+  {
+    for (LdapAttribute la : c) {
+      this.removeAttribute(la);
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void removeAttributes(final Attributes a)
+    throws NamingException
+  {
+    final NamingEnumeration<? extends Attribute> ne = a.getAll();
+    while (ne.hasMore()) {
+      final LdapAttribute la = this.beanFactory.newLdapAttribute();
+      la.setAttribute(ne.next());
+      this.removeAttribute(la);
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public int size()
+  {
+    return this.attributes.size();
+  }
+
+
+  /** {@inheritDoc} */
+  public void clear()
+  {
+    this.attributes.clear();
+  }
+
+
+  /** {@inheritDoc} */
+  public int hashCode()
+  {
+    int hc = HASH_CODE_SEED;
+    for (LdapAttribute a : this.attributes.values()) {
+      if (a != null) {
+        hc += a.hashCode();
+      }
+    }
+    return hc;
+  }
+
+
+  /**
+   * This returns a string representation of this object.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return String.format("%s", this.attributes.values());
+  }
+
+
+  /** {@inheritDoc} */
+  public Attributes toAttributes()
+  {
+    final Attributes attributes = new BasicAttributes(DEFAULT_IGNORE_CASE);
+    for (LdapAttribute a : this.attributes.values()) {
+      attributes.put(a.toAttribute());
+    }
+    return attributes;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapBean.java b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapBean.java
new file mode 100644
index 0000000..c68e6b6
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapBean.java
@@ -0,0 +1,73 @@
+/*
+  $Id: AbstractLdapBean.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractLdapBean</code> provides common implementations to other bean
+ * objects.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class AbstractLdapBean
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(getClass());
+
+  /** Factory for creating ldap beans. */
+  protected final LdapBeanFactory beanFactory;
+
+
+  /**
+   * Creates a new <code>AbstractLdapBean</code> with the supplied ldap bean
+   * factory.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public AbstractLdapBean(final LdapBeanFactory lbf)
+  {
+    this.beanFactory = lbf;
+  }
+
+
+  /**
+   * Returns whether the supplied <code>Object</code> contains the same data as
+   * this bean.
+   *
+   * @param  o  <code>Object</code>
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean equals(final Object o)
+  {
+    if (o == null) {
+      return false;
+    }
+    return
+      o == this ||
+        (this.getClass() == o.getClass() && o.hashCode() == this.hashCode());
+  }
+
+
+  /**
+   * This returns the hash code for this object.
+   *
+   * @return  <code>int</code>
+   */
+  public abstract int hashCode();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapEntry.java b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapEntry.java
new file mode 100644
index 0000000..49fa16a
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapEntry.java
@@ -0,0 +1,123 @@
+/*
+  $Id: AbstractLdapEntry.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>AbstractLdapEntry</code> provides a base implementation of <code>
+ * LdapEntry</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class AbstractLdapEntry extends AbstractLdapBean
+  implements LdapEntry
+{
+
+  /** hash code seed. */
+  protected static final int HASH_CODE_SEED = 43;
+
+  /** Distinguished name for this entry. */
+  protected String dn;
+
+  /** Attributes contained in this entry. */
+  protected LdapAttributes ldapAttributes;
+
+
+  /**
+   * Creates a new <code>AbstractLdapEntry</code> with the supplied ldap bean
+   * factory.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public AbstractLdapEntry(final LdapBeanFactory lbf)
+  {
+    super(lbf);
+  }
+
+
+  /** {@inheritDoc} */
+  public String getDn()
+  {
+    return this.dn;
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapAttributes getLdapAttributes()
+  {
+    return this.ldapAttributes;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setEntry(final SearchResult sr)
+    throws NamingException
+  {
+    this.setDn(sr.getName());
+
+    final LdapAttributes la = this.beanFactory.newLdapAttributes();
+    la.addAttributes(sr.getAttributes());
+    this.setLdapAttributes(la);
+  }
+
+
+  /** {@inheritDoc} */
+  public void setDn(final String dn)
+  {
+    this.dn = dn;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setLdapAttributes(final LdapAttributes a)
+  {
+    if (a != null) {
+      this.ldapAttributes = a;
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public int hashCode()
+  {
+    int hc = HASH_CODE_SEED;
+    if (this.getDn() != null) {
+      hc += this.getDn().hashCode();
+    }
+    hc += this.getLdapAttributes().hashCode();
+    return hc;
+  }
+
+
+  /**
+   * This returns a string representation of this object.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return String.format("dn=>%s%s", this.dn, this.ldapAttributes);
+  }
+
+
+  /** {@inheritDoc} */
+  public SearchResult toSearchResult()
+  {
+    return new SearchResult(this.dn, null, this.ldapAttributes.toAttributes());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapResult.java b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapResult.java
new file mode 100644
index 0000000..f39ed6d
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/AbstractLdapResult.java
@@ -0,0 +1,171 @@
+/*
+  $Id: AbstractLdapResult.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>AbstractLdapResult</code> provides a base implementation of <code>
+ * LdapResult</code> where the underlying entries are backed by a <code>
+ * Map</code>.
+ *
+ * @param  <T>  type of backing map
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class AbstractLdapResult<T extends Map<String, LdapEntry>>
+  extends AbstractLdapBean implements LdapResult
+{
+
+  /** hash code seed. */
+  protected static final int HASH_CODE_SEED = 44;
+
+  /** Entries contained in this result. */
+  protected T entries;
+
+
+  /**
+   * Creates a new <code>AbstractLdapResult</code> with the supplied ldap bean
+   * factory.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public AbstractLdapResult(final LdapBeanFactory lbf)
+  {
+    super(lbf);
+  }
+
+
+  /** {@inheritDoc} */
+  public Collection<LdapEntry> getEntries()
+  {
+    return this.entries.values();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapEntry getEntry(final String dn)
+  {
+    return this.entries.get(dn);
+  }
+
+
+  /** {@inheritDoc} */
+  public void addEntry(final LdapEntry e)
+  {
+    this.entries.put(e.getDn(), e);
+  }
+
+
+  /** {@inheritDoc} */
+  public void addEntry(final SearchResult sr)
+    throws NamingException
+  {
+    final LdapEntry le = this.beanFactory.newLdapEntry();
+    le.setEntry(sr);
+    this.addEntry(le);
+  }
+
+
+  /** {@inheritDoc} */
+  public void addEntries(final Collection<LdapEntry> c)
+  {
+    for (LdapEntry e : c) {
+      this.entries.put(e.getDn(), e);
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void addEntries(final NamingEnumeration<SearchResult> ne)
+    throws NamingException
+  {
+    while (ne.hasMore()) {
+      final LdapEntry le = this.beanFactory.newLdapEntry();
+      le.setEntry(ne.next());
+      this.addEntry(le);
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void addEntries(final Iterator<SearchResult> i)
+    throws NamingException
+  {
+    while (i.hasNext()) {
+      final LdapEntry le = this.beanFactory.newLdapEntry();
+      le.setEntry(i.next());
+      this.addEntry(le);
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public int size()
+  {
+    return this.entries.size();
+  }
+
+
+  /** {@inheritDoc} */
+  public void clear()
+  {
+    this.entries.clear();
+  }
+
+
+  /** {@inheritDoc} */
+  public int hashCode()
+  {
+    int hc = HASH_CODE_SEED;
+    for (LdapEntry e : this.entries.values()) {
+      if (e != null) {
+        hc += e.hashCode();
+      }
+    }
+    return hc;
+  }
+
+
+  /**
+   * This returns a string representation of this object.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return String.format("%s", this.entries.values());
+  }
+
+
+  /** {@inheritDoc} */
+  public List<SearchResult> toSearchResults()
+  {
+    final List<SearchResult> results = new ArrayList<SearchResult>(
+      this.entries.size());
+    for (LdapEntry e : this.entries.values()) {
+      results.add(e.toSearchResult());
+    }
+    return results;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/LdapAttribute.java b/src/main/java/edu/vt/middleware/ldap/bean/LdapAttribute.java
new file mode 100644
index 0000000..2912a4a
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/LdapAttribute.java
@@ -0,0 +1,84 @@
+/*
+  $Id: LdapAttribute.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.Set;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+
+/**
+ * <code>LdapAttribute</code> represents a single ldap attribute. Ldap attribute
+ * values must be unique per http://tools.ietf.org/html/rfc4512#section-2.3. For
+ * any given attribute, the values must all be of the same type.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapAttribute
+{
+
+
+  /**
+   * This returns the name of this <code>LdapAttribute</code>.
+   *
+   * @return  <code>String</code>
+   */
+  String getName();
+
+
+  /**
+   * This returns the value(s) of this <code>LdapAttribute</code>.
+   *
+   * @return  <code>Set</code>
+   */
+  Set<Object> getValues();
+
+
+  /**
+   * This returns the value(s) of this <code>LdapAttribute</code> Values are
+   * encoded in base64 format if the underlying value is of type byte[]. The
+   * returned set is unmodifiable.
+   *
+   * @return  unmodifiable <code>Set</code>
+   */
+  Set<String> getStringValues();
+
+
+  /**
+   * This sets this <code>LdapAttribute</code> using the supplied attribute.
+   *
+   * @param  attribute  <code>Attribute</code>
+   *
+   * @throws  NamingException  if the attribute values cannot be read
+   */
+  void setAttribute(final Attribute attribute)
+    throws NamingException;
+
+
+  /**
+   * This sets the name of this <code>LdapAttribute</code>.
+   *
+   * @param  name  <code>String</code>
+   */
+  void setName(final String name);
+
+
+  /**
+   * This returns an <code>Attribute</code> that represents the values in this
+   * <code>LdapAttribute</code>.
+   *
+   * @return  <code>Attribute</code>
+   */
+  Attribute toAttribute();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/LdapAttributes.java b/src/main/java/edu/vt/middleware/ldap/bean/LdapAttributes.java
new file mode 100644
index 0000000..0185e54
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/LdapAttributes.java
@@ -0,0 +1,166 @@
+/*
+  $Id: LdapAttributes.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.Collection;
+import java.util.List;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+
+/**
+ * <code>LdapAttributes</code> represents a collection of ldap attribute.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapAttributes
+{
+
+
+  /**
+   * This returns a <code>Collection</code> of <code>LdapAttribute</code> for
+   * this <code>LdapAttributes</code>.
+   *
+   * @return  <code>List</code>
+   */
+  Collection<LdapAttribute> getAttributes();
+
+
+  /**
+   * This returns the <code>LdapAttribute</code> for this <code>
+   * LdapAttributes</code> with the supplied name.
+   *
+   * @param  name  <code>String</code>
+   *
+   * @return  <code>LdapAttribute</code>
+   */
+  LdapAttribute getAttribute(final String name);
+
+
+  /**
+   * This returns an array of all the attribute names for this <code>
+   * LdapAttributes</code>.
+   *
+   * @return  <code>String[]</code>
+   */
+  String[] getAttributeNames();
+
+
+  /**
+   * This adds a new attribute to this <code>LdapAttributes</code>.
+   *
+   * @param  a  <code>LdapAttribute</code>
+   */
+  void addAttribute(final LdapAttribute a);
+
+
+  /**
+   * This adds a new attribute to this <code>LdapAttributes</code> with the
+   * supplied name and value.
+   *
+   * @param  name  <code>String</code>
+   * @param  value  <code>Object</code>
+   */
+  void addAttribute(final String name, final Object value);
+
+
+  /**
+   * This adds a new attribute to this <code>LdapAttributes</code> with the
+   * supplied name and values.
+   *
+   * @param  name  <code>String</code>
+   * @param  values  <code>List</code>
+   */
+  void addAttribute(final String name, final List<?> values);
+
+
+  /**
+   * This adds a <code>Collection</code> of attributes to this <code>
+   * LdapAttributes</code>. The collection should contain <code>
+   * LdapAttribute</code> objects.
+   *
+   * @param  c  <code>Collection</code>
+   */
+  void addAttributes(final Collection<LdapAttribute> c);
+
+
+  /**
+   * This adds the attributes in the supplied <code>Attributes</code> to this
+   * <code>LdapAttributes</code>.
+   *
+   * @param  a  <code>Attributes</code>
+   *
+   * @throws  NamingException  if the attributes cannot be read
+   */
+  void addAttributes(final Attributes a)
+    throws NamingException;
+
+
+  /**
+   * This removes an attribute from this <code>LdapAttributes</code>.
+   *
+   * @param  a  <code>LdapAttribute</code>
+   */
+  void removeAttribute(final LdapAttribute a);
+
+
+  /**
+   * This removes the attribute with the supplied name.
+   *
+   * @param  name  <code>String</code>
+   */
+  void removeAttribute(final String name);
+
+
+  /**
+   * This removes a <code>Collection</code> of attributes from this <code>
+   * LdapAttributes</code>. The collection should contain <code>
+   * LdapAttribute</code> objects.
+   *
+   * @param  c  <code>Collection</code>
+   */
+  void removeAttributes(final Collection<LdapAttribute> c);
+
+
+  /**
+   * This removes the attributes in the supplied <code>Attributes</code> from
+   * this <code>LdapAttributes</code>.
+   *
+   * @param  a  <code>Attributes</code>
+   *
+   * @throws  NamingException  if the attributes cannot be read
+   */
+  void removeAttributes(final Attributes a)
+    throws NamingException;
+
+
+  /**
+   * This returns the number of attributes in this attributes.
+   *
+   * @return  <code>int</code>
+   */
+  int size();
+
+
+  /** This removes all attributes from this <code>LdapAttributes</code>. */
+  void clear();
+
+
+  /**
+   * This returns an <code>Attributes</code> that represents this entry.
+   *
+   * @return  <code>Attributes</code>
+   */
+  Attributes toAttributes();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/LdapBeanFactory.java b/src/main/java/edu/vt/middleware/ldap/bean/LdapBeanFactory.java
new file mode 100644
index 0000000..bbb4f62
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/LdapBeanFactory.java
@@ -0,0 +1,57 @@
+/*
+  $Id: LdapBeanFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+/**
+ * <code>LdapBeanFactory</code> provides an interface for ldap bean type
+ * factories.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapBeanFactory
+{
+
+
+  /**
+   * Create a new instance of <code>LdapResult</code>.
+   *
+   * @return  <code>LdapResult</code>
+   */
+  LdapResult newLdapResult();
+
+
+  /**
+   * Create a new instance of <code>LdapEntry</code>.
+   *
+   * @return  <code>LdapEntry</code>
+   */
+  LdapEntry newLdapEntry();
+
+
+  /**
+   * Create a new instance of <code>LdapAttributes</code>.
+   *
+   * @return  <code>LdapAttributes</code>
+   */
+  LdapAttributes newLdapAttributes();
+
+
+  /**
+   * Create a new instance of <code>LdapAttribute</code>.
+   *
+   * @return  <code>LdapAttribute</code>
+   */
+  LdapAttribute newLdapAttribute();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/LdapBeanProvider.java b/src/main/java/edu/vt/middleware/ldap/bean/LdapBeanProvider.java
new file mode 100644
index 0000000..a8c9bfd
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/LdapBeanProvider.java
@@ -0,0 +1,107 @@
+/*
+  $Id: LdapBeanProvider.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>LdapBeanProvider</code> provides a single source for ldap bean types
+ * and configuration.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class LdapBeanProvider
+{
+
+  /** bean factory class name. */
+  public static final String BEAN_FACTORY =
+    "edu.vt.middleware.ldap.beanFactory";
+
+  /** Log for this class. */
+  private static final Log LOG = LogFactory.getLog(LdapBeanProvider.class);
+
+  /** single instance of the ldap bean provider. */
+  private static final LdapBeanProvider INSTANCE = new LdapBeanProvider();
+
+  /** factory used to create ldap beans. */
+  private static LdapBeanFactory beanFactory;
+
+
+  /** Default constructor. */
+  private LdapBeanProvider()
+  {
+    final String beanFactoryClass = System.getProperty(BEAN_FACTORY);
+    if (beanFactoryClass != null) {
+      try {
+        beanFactory = (LdapBeanFactory) Class.forName(beanFactoryClass)
+            .newInstance();
+        if (LOG.isInfoEnabled()) {
+          LOG.info("Set provider bean factory to " + beanFactoryClass);
+        }
+      } catch (ClassNotFoundException e) {
+        if (LOG.isErrorEnabled()) {
+          LOG.error("Error instantiating " + beanFactoryClass, e);
+        }
+      } catch (InstantiationException e) {
+        if (LOG.isErrorEnabled()) {
+          LOG.error("Error instantiating " + beanFactoryClass, e);
+        }
+      } catch (IllegalAccessException e) {
+        if (LOG.isErrorEnabled()) {
+          LOG.error("Error instantiating " + beanFactoryClass, e);
+        }
+      }
+    } else {
+      // set default ldap bean factory to unordered
+      beanFactory = new UnorderedLdapBeanFactory();
+    }
+  }
+
+
+  /**
+   * Returns the instance of this <code>LdapBeanProvider</code>.
+   *
+   * @return  <code>LdapBeanProvider</code>
+   */
+  public static LdapBeanProvider getInstance()
+  {
+    return INSTANCE;
+  }
+
+
+  /**
+   * Returns the factory for creating ldap beans.
+   *
+   * @return  <code>LdapBeanFactory</code>
+   */
+  public static LdapBeanFactory getLdapBeanFactory()
+  {
+    return beanFactory;
+  }
+
+
+  /**
+   * Sets the factory for creating ldap beans.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public static void setLdapBeanFactory(final LdapBeanFactory lbf)
+  {
+    if (lbf != null) {
+      beanFactory = lbf;
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/LdapEntry.java b/src/main/java/edu/vt/middleware/ldap/bean/LdapEntry.java
new file mode 100644
index 0000000..c7fe3da
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/LdapEntry.java
@@ -0,0 +1,79 @@
+/*
+  $Id: LdapEntry.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>LdapEntry</code> represents a single ldap entry.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapEntry
+{
+
+
+  /**
+   * This returns the DN for this <code>LdapEntry</code>.
+   *
+   * @return  <code>String</code>
+   */
+  String getDn();
+
+
+  /**
+   * This returns the <code>LdapAttributes</code> for this <code>
+   * LdapEntry</code>.
+   *
+   * @return  <code>LdapAttributes</code>
+   */
+  LdapAttributes getLdapAttributes();
+
+
+  /**
+   * This sets this <code>LdapEntry</code> with the supplied search result.
+   *
+   * @param  sr  <code>SearchResult</code>
+   *
+   * @throws  NamingException  if the search result cannot be read
+   */
+  void setEntry(final SearchResult sr)
+    throws NamingException;
+
+
+  /**
+   * This sets the DN for this <code>LdapEntry</code>.
+   *
+   * @param  dn  <code>String</code>
+   */
+  void setDn(final String dn);
+
+
+  /**
+   * This sets the attributes for this <code>LdapEntry</code>.
+   *
+   * @param  a  <code>LdapAttribute</code>
+   */
+  void setLdapAttributes(final LdapAttributes a);
+
+
+  /**
+   * This returns a <code>SearchResult</code> that represents this entry.
+   *
+   * @return  <code>SearchResult</code>
+   */
+  SearchResult toSearchResult();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/LdapResult.java b/src/main/java/edu/vt/middleware/ldap/bean/LdapResult.java
new file mode 100644
index 0000000..6bc5b20
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/LdapResult.java
@@ -0,0 +1,124 @@
+/*
+  $Id: LdapResult.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>LdapResult</code> represents a collection of ldap entries.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapResult
+{
+
+
+  /**
+   * This returns a <code>Collection</code> of <code>LdapEntry</code> for this
+   * <code>LdapResult</code>.
+   *
+   * @return  <code>Collection</code>
+   */
+  Collection<LdapEntry> getEntries();
+
+
+  /**
+   * This returns the <code>LdapEntry</code> for this <code>LdapResult</code>
+   * with the supplied DN.
+   *
+   * @param  dn  <code>String</code>
+   *
+   * @return  <code>LdapEntry</code>
+   */
+  LdapEntry getEntry(final String dn);
+
+
+  /**
+   * This adds a new entry to this <code>LdapResult</code>.
+   *
+   * @param  e  <code>LdapEntry</code>
+   */
+  void addEntry(final LdapEntry e);
+
+
+  /**
+   * This adds a new entry to this <code>LdapResult</code>.
+   *
+   * @param  sr  <code>SearchResult</code>
+   *
+   * @throws  NamingException  if the search results cannot be read
+   */
+  void addEntry(final SearchResult sr)
+    throws NamingException;
+
+
+  /**
+   * This adds a <code>Collection</code> of entries to this <code>
+   * LdapResult</code>. The list should contain <code>LdapEntry</code> objects.
+   *
+   * @param  c  <code>Collection</code>
+   */
+  void addEntries(final Collection<LdapEntry> c);
+
+
+  /**
+   * This adds a <code>NamingEnumeration</code> of <code>SearchResult</code> to
+   * this <code>LdapResult</code>.
+   *
+   * @param  ne  <code>NamingEnumeration</code>
+   *
+   * @throws  NamingException  if the search results cannot be read
+   */
+  void addEntries(final NamingEnumeration<SearchResult> ne)
+    throws NamingException;
+
+
+  /**
+   * This adds an <code>Iterator</code> of <code>SearchResult</code> to this
+   * <code>LdapResult</code>.
+   *
+   * @param  i  <code>Iterator</code>
+   *
+   * @throws  NamingException  if the search results cannot be read
+   */
+  void addEntries(final Iterator<SearchResult> i)
+    throws NamingException;
+
+
+  /**
+   * This returns the number of entries in this result.
+   *
+   * @return  <code>int</code>
+   */
+  int size();
+
+
+  /** This removes all entries from this <code>LdapResult</code>. */
+  void clear();
+
+
+  /**
+   * This returns a <code>List</code> of <code>SearchResult</code> that
+   * represent the entries in this <code>LdapResult</code>.
+   *
+   * @return  <code>List</code>
+   */
+  List<SearchResult> toSearchResults();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/OrderedLdapBeanFactory.java b/src/main/java/edu/vt/middleware/ldap/bean/OrderedLdapBeanFactory.java
new file mode 100644
index 0000000..2602d51
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/OrderedLdapBeanFactory.java
@@ -0,0 +1,135 @@
+/*
+  $Id: OrderedLdapBeanFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * <code>OrderedLdapBeanFactory</code> provides an ldap bean factory that
+ * produces ordered ldap beans.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class OrderedLdapBeanFactory implements LdapBeanFactory
+{
+
+
+  /** {@inheritDoc} */
+  public LdapResult newLdapResult()
+  {
+    return new OrderedLdapResult();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapEntry newLdapEntry()
+  {
+    return new OrderedLdapEntry();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapAttributes newLdapAttributes()
+  {
+    return new OrderedLdapAttributes();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapAttribute newLdapAttribute()
+  {
+    return new OrderedLdapAttribute();
+  }
+
+
+  /**
+   * <code>OrderedLdapResult</code> represents a collection of ldap entries that
+   * are ordered by insertion.
+   */
+  protected class OrderedLdapResult
+    extends AbstractLdapResult<LinkedHashMap<String, LdapEntry>>
+  {
+
+
+    /** Default constructor. */
+    public OrderedLdapResult()
+    {
+      super(OrderedLdapBeanFactory.this);
+      this.entries = new LinkedHashMap<String, LdapEntry>();
+    }
+  }
+
+
+  /** <code>OrderedLdapEntry</code> represents a single ldap entry. */
+  protected class OrderedLdapEntry extends AbstractLdapEntry
+  {
+
+
+    /** Default constructor. */
+    public OrderedLdapEntry()
+    {
+      super(OrderedLdapBeanFactory.this);
+      this.ldapAttributes = new OrderedLdapAttributes();
+    }
+  }
+
+
+  /**
+   * <code>OrderedLdapAttributes</code> represents a collection of ldap
+   * attribute that are ordered by insertion.
+   */
+  protected class OrderedLdapAttributes
+    extends AbstractLdapAttributes<LinkedHashMap<String, LdapAttribute>>
+  {
+
+
+    /** Default constructor. */
+    public OrderedLdapAttributes()
+    {
+      super(OrderedLdapBeanFactory.this);
+      this.attributes = new LinkedHashMap<String, LdapAttribute>();
+    }
+  }
+
+
+  /**
+   * <code>OrderedLdapAttribute</code> represents a single ldap attribute whose
+   * values are ordered by insertion.
+   */
+  protected class OrderedLdapAttribute
+    extends AbstractLdapAttribute<LinkedHashSet<Object>>
+  {
+
+
+    /** Default constructor. */
+    public OrderedLdapAttribute()
+    {
+      super(OrderedLdapBeanFactory.this);
+      this.values = new LinkedHashSet<Object>();
+    }
+
+
+    /** {@inheritDoc} */
+    public Set<String> getStringValues()
+    {
+      final Set<String> s = new LinkedHashSet<String>();
+      this.convertValuesToString(s);
+      return Collections.unmodifiableSet(s);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/SortedLdapBeanFactory.java b/src/main/java/edu/vt/middleware/ldap/bean/SortedLdapBeanFactory.java
new file mode 100644
index 0000000..c747801
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/SortedLdapBeanFactory.java
@@ -0,0 +1,137 @@
+/*
+  $Id: SortedLdapBeanFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * <code>SortedLdapBeanFactory</code> provides an ldap bean factory that
+ * produces sorted ldap beans.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class SortedLdapBeanFactory implements LdapBeanFactory
+{
+
+
+  /** {@inheritDoc} */
+  public LdapResult newLdapResult()
+  {
+    return new SortedLdapResult();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapEntry newLdapEntry()
+  {
+    return new SortedLdapEntry();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapAttributes newLdapAttributes()
+  {
+    return new SortedLdapAttributes();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapAttribute newLdapAttribute()
+  {
+    return new SortedLdapAttribute();
+  }
+
+
+  /**
+   * <code>SortedLdapResult</code> represents a collection of ldap entries that
+   * are sorted by their DN.
+   */
+  protected class SortedLdapResult
+    extends AbstractLdapResult<TreeMap<String, LdapEntry>>
+  {
+
+
+    /** Default constructor. */
+    public SortedLdapResult()
+    {
+      super(SortedLdapBeanFactory.this);
+      this.entries = new TreeMap<String, LdapEntry>(
+        String.CASE_INSENSITIVE_ORDER);
+    }
+  }
+
+
+  /** <code>SortedLdapEntry</code> represents a single ldap entry. */
+  protected class SortedLdapEntry extends AbstractLdapEntry
+  {
+
+
+    /** Default constructor. */
+    public SortedLdapEntry()
+    {
+      super(SortedLdapBeanFactory.this);
+      this.ldapAttributes = new SortedLdapAttributes();
+    }
+  }
+
+
+  /**
+   * <code>SortedLdapAttributes</code> represents a collection of ldap attribute
+   * that are sorted by their name.
+   */
+  protected class SortedLdapAttributes
+    extends AbstractLdapAttributes<TreeMap<String, LdapAttribute>>
+  {
+
+
+    /** Default constructor. */
+    public SortedLdapAttributes()
+    {
+      super(SortedLdapBeanFactory.this);
+      this.attributes = new TreeMap<String, LdapAttribute>(
+        String.CASE_INSENSITIVE_ORDER);
+    }
+  }
+
+
+  /**
+   * <code>SortedLdapAttribute</code> represents a single ldap attribute whose
+   * values are sorted.
+   */
+  protected class SortedLdapAttribute
+    extends AbstractLdapAttribute<TreeSet<Object>>
+  {
+
+
+    /** Default constructor. */
+    public SortedLdapAttribute()
+    {
+      super(SortedLdapBeanFactory.this);
+      this.values = new TreeSet<Object>();
+    }
+
+
+    /** {@inheritDoc} */
+    public Set<String> getStringValues()
+    {
+      final Set<String> s = new TreeSet<String>();
+      this.convertValuesToString(s);
+      return Collections.unmodifiableSet(s);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/bean/UnorderedLdapBeanFactory.java b/src/main/java/edu/vt/middleware/ldap/bean/UnorderedLdapBeanFactory.java
new file mode 100644
index 0000000..23eb1ad
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/bean/UnorderedLdapBeanFactory.java
@@ -0,0 +1,135 @@
+/*
+  $Id: UnorderedLdapBeanFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * <code>UnorderedLdapBeanFactory</code> provides an ldap bean factory that
+ * produces unordered ldap beans.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class UnorderedLdapBeanFactory implements LdapBeanFactory
+{
+
+
+  /** {@inheritDoc} */
+  public LdapResult newLdapResult()
+  {
+    return new UnorderedLdapResult();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapEntry newLdapEntry()
+  {
+    return new UnorderedLdapEntry();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapAttributes newLdapAttributes()
+  {
+    return new UnorderedLdapAttributes();
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapAttribute newLdapAttribute()
+  {
+    return new UnorderedLdapAttribute();
+  }
+
+
+  /**
+   * <code>UnorderedLdapResult</code> represents a collection of ldap entries
+   * that are unordered.
+   */
+  protected class UnorderedLdapResult
+    extends AbstractLdapResult<HashMap<String, LdapEntry>>
+  {
+
+
+    /** Default constructor. */
+    public UnorderedLdapResult()
+    {
+      super(UnorderedLdapBeanFactory.this);
+      this.entries = new HashMap<String, LdapEntry>();
+    }
+  }
+
+
+  /** <code>UnorderedLdapEntry</code> represents a single ldap entry. */
+  protected class UnorderedLdapEntry extends AbstractLdapEntry
+  {
+
+
+    /** Default constructor. */
+    public UnorderedLdapEntry()
+    {
+      super(UnorderedLdapBeanFactory.this);
+      this.ldapAttributes = new UnorderedLdapAttributes();
+    }
+  }
+
+
+  /**
+   * <code>UnorderedLdapAttributes</code> represents a collection of ldap
+   * attribute that are unordered.
+   */
+  protected class UnorderedLdapAttributes
+    extends AbstractLdapAttributes<HashMap<String, LdapAttribute>>
+  {
+
+
+    /** Default constructor. */
+    public UnorderedLdapAttributes()
+    {
+      super(UnorderedLdapBeanFactory.this);
+      this.attributes = new HashMap<String, LdapAttribute>();
+    }
+  }
+
+
+  /**
+   * <code>UnorderedLdapAttribute</code> represents a single ldap attribute
+   * whose values are unordered.
+   */
+  protected class UnorderedLdapAttribute
+    extends AbstractLdapAttribute<HashSet<Object>>
+  {
+
+
+    /** Default constructor. */
+    public UnorderedLdapAttribute()
+    {
+      super(UnorderedLdapBeanFactory.this);
+      this.values = new HashSet<Object>();
+    }
+
+
+    /** {@inheritDoc} */
+    public Set<String> getStringValues()
+    {
+      final Set<String> s = new HashSet<String>();
+      this.convertValuesToString(s);
+      return Collections.unmodifiableSet(s);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/dsml/AbstractDsml.java b/src/main/java/edu/vt/middleware/ldap/dsml/AbstractDsml.java
new file mode 100644
index 0000000..c2a9199
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/dsml/AbstractDsml.java
@@ -0,0 +1,387 @@
+/*
+  $Id: AbstractDsml.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.dsml;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.LdapUtil;
+import edu.vt.middleware.ldap.bean.LdapAttribute;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapBeanFactory;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.dom4j.Document;
+import org.dom4j.DocumentException;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.Namespace;
+import org.dom4j.QName;
+import org.dom4j.io.OutputFormat;
+import org.dom4j.io.SAXReader;
+import org.dom4j.io.XMLWriter;
+
+/**
+ * <code>AbstractDsml</code> contains functions for converting LDAP search
+ * result sets into DSML.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class AbstractDsml implements Serializable
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -5626050181955100494L;
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Ldap bean factory. */
+  protected LdapBeanFactory beanFactory = LdapBeanProvider.getLdapBeanFactory();
+
+
+  /**
+   * Returns the factory for creating ldap beans.
+   *
+   * @return  <code>LdapBeanFactory</code>
+   */
+  public LdapBeanFactory getLdapBeanFactory()
+  {
+    return this.beanFactory;
+  }
+
+
+  /**
+   * Sets the factory for creating ldap beans.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public void setLdapBeanFactory(final LdapBeanFactory lbf)
+  {
+    if (lbf != null) {
+      this.beanFactory = lbf;
+    }
+  }
+
+
+  /**
+   * This will take the results of a prior LDAP query and convert it to a DSML
+   * <code>Document</code>.
+   *
+   * @param  results  <code>Iterator</code> of LDAP search results
+   *
+   * @return  <code>Document</code>
+   */
+  public abstract Document createDsml(final Iterator<SearchResult> results);
+
+
+  /**
+   * This will take the results of a prior LDAP query and convert it to a DSML
+   * <code>Document</code>.
+   *
+   * @param  result  <code>LdapResult</code>
+   *
+   * @return  <code>Document</code>
+   */
+  public abstract Document createDsml(final LdapResult result);
+
+
+  /**
+   * This will take an LDAP search result and convert it to a DSML entry
+   * element.
+   *
+   * @param  entryName  <code>QName</code> name of element to create
+   * @param  ldapEntry  <code>LdapEntry</code> to convert
+   * @param  ns  <code>Namespace</code> of DSML
+   *
+   * @return  <code>Document</code>
+   */
+  protected Element createDsmlEntry(
+    final QName entryName,
+    final LdapEntry ldapEntry,
+    final Namespace ns)
+  {
+    // create Element to hold result content
+    final Element entryElement = DocumentHelper.createElement(entryName);
+
+    if (ldapEntry != null) {
+
+      final String dn = ldapEntry.getDn();
+      if (dn != null) {
+        entryElement.addAttribute("dn", dn);
+      }
+
+      for (Element e :
+           createDsmlAttributes(ldapEntry.getLdapAttributes(), ns)) {
+        entryElement.add(e);
+      }
+    }
+
+    return entryElement;
+  }
+
+
+  /**
+   * This will return a list of DSML attribute elements from the supplied <code>
+   * LdapAttributes</code>.
+   *
+   * @param  ldapAttributes  <code>LdapAttributes</code>
+   * @param  ns  <code>Namespace</code> of DSML
+   *
+   * @return  <code>List</code> of elements
+   */
+  protected List<Element> createDsmlAttributes(
+    final LdapAttributes ldapAttributes,
+    final Namespace ns)
+  {
+    final List<Element> attrElements = new ArrayList<Element>();
+    for (LdapAttribute attr : ldapAttributes.getAttributes()) {
+      final String attrName = attr.getName();
+      final Set<?> attrValues = attr.getValues();
+      final Element attrElement = createDsmlAttribute(
+        attrName,
+        attrValues,
+        ns,
+        "attr",
+        "name",
+        "value");
+      if (attrElement.hasContent()) {
+        attrElements.add(attrElement);
+      }
+    }
+    return attrElements;
+  }
+
+
+  /**
+   * This will take an attribute name and it's values and return a DSML
+   * attribute element.
+   *
+   * @param  attrName  <code>String</code>
+   * @param  attrValues  <code>Set</code>
+   * @param  ns  <code>Namespace</code> of DSML
+   * @param  elementName  <code>String</code> of the attribute element
+   * @param  elementAttrName  <code>String</code> of the attribute element
+   * @param  elementValueName  <code>String</code> of the value element
+   *
+   * @return  <code>Element</code>
+   */
+  protected Element createDsmlAttribute(
+    final String attrName,
+    final Set<?> attrValues,
+    final Namespace ns,
+    final String elementName,
+    final String elementAttrName,
+    final String elementValueName)
+  {
+    final Element attrElement = DocumentHelper.createElement("");
+
+    if (attrName != null) {
+
+      attrElement.setQName(new QName(elementName, ns));
+      if (elementAttrName != null) {
+        attrElement.addAttribute(elementAttrName, attrName);
+      }
+      if (attrValues != null) {
+        final Iterator<?> i = attrValues.iterator();
+        while (i.hasNext()) {
+          final Object rawValue = i.next();
+          String value = null;
+          boolean isBase64 = false;
+          if (rawValue instanceof String) {
+            value = (String) rawValue;
+          } else if (rawValue instanceof byte[]) {
+            value = LdapUtil.base64Encode((byte[]) rawValue);
+            isBase64 = true;
+          } else {
+            if (this.logger.isWarnEnabled()) {
+              this.logger.warn(
+                "Could not cast attribute value as a byte[]" +
+                " or a String");
+            }
+          }
+          if (value != null) {
+            final Element valueElement = attrElement.addElement(
+              new QName(elementValueName, ns));
+            valueElement.addText(value);
+            if (isBase64) {
+              valueElement.addAttribute("encoding", "base64");
+            }
+          }
+        }
+      }
+    }
+
+    return attrElement;
+  }
+
+
+  /**
+   * This will write the supplied LDAP search results to the supplied writer in
+   * the form of DSML.
+   *
+   * @param  results  <code>Iterator</code> of LDAP search results
+   * @param  writer  <code>Writer</code> to write to
+   *
+   * @throws  IOException  if an error occurs while writing
+   */
+  public void outputDsml(
+    final Iterator<SearchResult> results,
+    final Writer writer)
+    throws IOException
+  {
+    final XMLWriter xmlWriter = new XMLWriter(
+      writer,
+      OutputFormat.createPrettyPrint());
+    xmlWriter.write(createDsml(results));
+    writer.flush();
+  }
+
+
+  /**
+   * This will write the supplied LDAP result to the supplied writer in the form
+   * of DSML.
+   *
+   * @param  result  <code>LdapResult</code>
+   * @param  writer  <code>Writer</code> to write to
+   *
+   * @throws  IOException  if an error occurs while writing
+   */
+  public void outputDsml(final LdapResult result, final Writer writer)
+    throws IOException
+  {
+    final XMLWriter xmlWriter = new XMLWriter(
+      writer,
+      OutputFormat.createPrettyPrint());
+    xmlWriter.write(createDsml(result));
+    writer.flush();
+  }
+
+
+  /**
+   * This will take a Reader containing a DSML <code>Document</code> and convert
+   * it to an Iterator of LDAP search results.
+   *
+   * @param  reader  <code>Reader</code> containing DSML content
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  DocumentException  if an error occurs building a document from the
+   * reader
+   * @throws  IOException  if an I/O error occurs
+   */
+  public Iterator<SearchResult> importDsml(final Reader reader)
+    throws DocumentException, IOException
+  {
+    final Document dsml = new SAXReader().read(reader);
+    return createLdapResult(dsml).toSearchResults().iterator();
+  }
+
+
+  /**
+   * This will take a Reader containing a DSML <code>Document</code> and convert
+   * it to an <code>LdapResult</code>.
+   *
+   * @param  reader  <code>Reader</code> containing DSML content
+   *
+   * @return  <code>LdapResult</code>
+   *
+   * @throws  DocumentException  if an error occurs building a document from the
+   * reader
+   * @throws  IOException  if an I/O error occurs
+   */
+  public LdapResult importDsmlToLdapResult(final Reader reader)
+    throws DocumentException, IOException
+  {
+    final Document dsml = new SAXReader().read(reader);
+    return createLdapResult(dsml);
+  }
+
+
+  /**
+   * This will take a DSML <code>Document</code> and convert it to an Iterator
+   * of LDAP search results.
+   *
+   * @param  doc  <code>Document</code> of DSML
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   */
+  protected abstract LdapResult createLdapResult(final Document doc);
+
+
+  /**
+   * This will take a DSML <code>Element</code> containing an entry of type
+   * <entry/> and convert it to an LDAP entry.
+   *
+   * @param  entryElement  <code>Element</code> of DSML content
+   *
+   * @return  <code>LdapEntry</code>
+   */
+  protected LdapEntry createLdapEntry(final Element entryElement)
+  {
+    final LdapEntry ldapEntry = this.beanFactory.newLdapEntry();
+    ldapEntry.setDn("");
+
+    if (entryElement != null) {
+
+      final String name = entryElement.attributeValue("dn");
+      if (name != null) {
+        ldapEntry.setDn(name);
+      }
+
+      if (entryElement.hasContent()) {
+
+        // load the attribute elements
+        final Iterator<?> attrIterator = entryElement.elementIterator("attr");
+        while (attrIterator.hasNext()) {
+          final Element attrElement = (Element) attrIterator.next();
+          final String attrName = attrElement.attributeValue("name");
+          if (attrName != null && attrElement.hasContent()) {
+            final LdapAttribute ldapAttribute = this.beanFactory
+                .newLdapAttribute();
+            ldapAttribute.setName(attrName);
+
+            final Iterator<?> valueIterator = attrElement.elementIterator(
+              "value");
+            while (valueIterator.hasNext()) {
+              final Element valueElement = (Element) valueIterator.next();
+              final String value = valueElement.getText();
+              if (value != null) {
+                final String encoding = valueElement.attributeValue("encoding");
+                if (encoding != null && "base64".equals(encoding)) {
+                  ldapAttribute.getValues().add(LdapUtil.base64Decode(value));
+                } else {
+                  ldapAttribute.getValues().add(value);
+                }
+              }
+            }
+            ldapEntry.getLdapAttributes().addAttribute(ldapAttribute);
+          }
+        }
+      }
+    }
+
+    return ldapEntry;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/dsml/DsmlResultConverter.java b/src/main/java/edu/vt/middleware/ldap/dsml/DsmlResultConverter.java
new file mode 100644
index 0000000..bddd1c6
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/dsml/DsmlResultConverter.java
@@ -0,0 +1,174 @@
+/*
+  $Id: DsmlResultConverter.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.dsml;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.bean.LdapBeanFactory;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.dom4j.DocumentException;
+
+/**
+ * <code>DsmlResultConverter</code> provides utility methods for converting
+ * <code>LdapResult</code> to and from DSML in string format.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class DsmlResultConverter
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(getClass());
+
+  /** Ldap bean factory. */
+  protected LdapBeanFactory beanFactory = LdapBeanProvider.getLdapBeanFactory();
+
+  /** Class for outputting version 1 DSML. */
+  private Dsmlv1 dsmlv1 = new Dsmlv1();
+
+  /** Class for outputting version 2 DSML. */
+  private Dsmlv2 dsmlv2 = new Dsmlv2();
+
+
+  /**
+   * Returns the factory for creating ldap beans.
+   *
+   * @return  <code>LdapBeanFactory</code>
+   */
+  public LdapBeanFactory getLdapBeanFactory()
+  {
+    return this.beanFactory;
+  }
+
+
+  /**
+   * Sets the factory for creating ldap beans.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public void setLdapBeanFactory(final LdapBeanFactory lbf)
+  {
+    if (lbf != null) {
+      this.beanFactory = lbf;
+      this.dsmlv1.setLdapBeanFactory(lbf);
+      this.dsmlv2.setLdapBeanFactory(lbf);
+    }
+  }
+
+
+  /**
+   * This returns this <code>DsmlResult</code> as version 1 DSML.
+   *
+   * @param  result  <code>LdapResult</code> to convert
+   *
+   * @return  <code>String</code>
+   */
+  public String toDsmlv1(final LdapResult result)
+  {
+    final StringWriter writer = new StringWriter();
+    try {
+      this.dsmlv1.outputDsml(result.toSearchResults().iterator(), writer);
+    } catch (IOException e) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("Could not write dsml to StringWriter", e);
+      }
+    }
+    return writer.toString();
+  }
+
+
+  /**
+   * This reads any entries in the supplied DSML into this <code>
+   * DsmlResult</code>.
+   *
+   * @param  dsml  <code>String</code> to read
+   *
+   * @return  <code>LdapResult</code>
+   *
+   * @throws  DocumentException  if an error occurs reading the supplied DSML
+   */
+  public LdapResult fromDsmlv1(final String dsml)
+    throws DocumentException
+  {
+    final LdapResult result = this.beanFactory.newLdapResult();
+    try {
+      result.addEntries(this.dsmlv1.importDsml(new StringReader(dsml)));
+    } catch (IOException e) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("Could not read dsml from StringReader", e);
+      }
+    } catch (NamingException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Unexpected naming exception occurred", e);
+      }
+    }
+    return result;
+  }
+
+
+  /**
+   * This returns this <code>DsmlResult</code> as version 2 DSML.
+   *
+   * @param  result  <code>LdapResult</code> to convert
+   *
+   * @return  <code>String</code>
+   */
+  public String toDsmlv2(final LdapResult result)
+  {
+    final StringWriter writer = new StringWriter();
+    try {
+      this.dsmlv2.outputDsml(result.toSearchResults().iterator(), writer);
+    } catch (IOException e) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("Could not write dsml to StringWriter", e);
+      }
+    }
+    return writer.toString();
+  }
+
+
+  /**
+   * This reads any entries in the supplied DSML into this <code>
+   * DsmlResult</code>.
+   *
+   * @param  dsml  <code>String</code> to read
+   *
+   * @return  <code>LdapResult</code>
+   *
+   * @throws  DocumentException  if an error occurs reading the supplied DSML
+   */
+  public LdapResult fromDsmlv2(final String dsml)
+    throws DocumentException
+  {
+    final LdapResult result = this.beanFactory.newLdapResult();
+    try {
+      result.addEntries(this.dsmlv2.importDsml(new StringReader(dsml)));
+    } catch (IOException e) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("Could not read dsml from StringReader", e);
+      }
+    } catch (NamingException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Unexpected naming exception occurred", e);
+      }
+    }
+    return result;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/dsml/DsmlSearch.java b/src/main/java/edu/vt/middleware/ldap/dsml/DsmlSearch.java
new file mode 100644
index 0000000..f1a59e8
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/dsml/DsmlSearch.java
@@ -0,0 +1,112 @@
+/*
+  $Id: DsmlSearch.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.dsml;
+
+import java.io.IOException;
+import java.io.Writer;
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapSearch;
+import edu.vt.middleware.ldap.pool.LdapPool;
+
+/**
+ * <code>DsmlSearch</code> queries an LDAP and returns the result as DSML. Each
+ * instance of <code>DsmlSearch</code> maintains it's own pool of LDAP
+ * connections.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class DsmlSearch extends LdapSearch
+{
+
+  /** Valid DSML versions. */
+  public enum Version {
+
+    /** DSML version 1. */
+    ONE,
+
+    /** DSML version 2. */
+    TWO
+  }
+
+  /** Version of DSML to produce, default is 1. */
+  private Version version = Version.ONE;
+
+  /** Dsml version 1 object. */
+  private Dsmlv1 dsmlv1 = new Dsmlv1();
+
+  /** Dsml version 2 object. */
+  private Dsmlv2 dsmlv2 = new Dsmlv2();
+
+
+  /**
+   * This creates a new <code>DsmlSearch</code> with the supplied pool.
+   *
+   * @param  pool  <code>LdapPool</code>
+   */
+  public DsmlSearch(final LdapPool<Ldap> pool)
+  {
+    super(pool);
+  }
+
+
+  /**
+   * This gets the version of dsml to produce.
+   *
+   * @return  <code>Version</code> of DSML to produce
+   */
+  public Version getVersion()
+  {
+    return this.version;
+  }
+
+
+  /**
+   * This sets the version of dsml to produce.
+   *
+   * @param  v  <code>Version</code> of DSML to produce
+   */
+  public void setVersion(final Version v)
+  {
+    this.version = v;
+  }
+
+
+  /**
+   * This will perform an LDAP search with the supplied query and return
+   * attributes. The results will be written to the supplied <code>
+   * Writer</code>. Use {@link #version} to control which version of DSML is
+   * written.
+   *
+   * @param  query  <code>String</code> to search for
+   * @param  attrs  <code>String[]</code> to return
+   * @param  writer  <code>Writer</code> to write to
+   *
+   * @throws  NamingException  if an error occurs while searching
+   * @throws  IOException  if an error occurs while writing search results
+   */
+  public void search(
+    final String query,
+    final String[] attrs,
+    final Writer writer)
+    throws NamingException, IOException
+  {
+    if (this.version == Version.TWO) {
+      this.dsmlv2.outputDsml(this.search(query, attrs), writer);
+    } else {
+      this.dsmlv1.outputDsml(this.search(query, attrs), writer);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/dsml/Dsmlv1.java b/src/main/java/edu/vt/middleware/ldap/dsml/Dsmlv1.java
new file mode 100644
index 0000000..dec8ec5
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/dsml/Dsmlv1.java
@@ -0,0 +1,247 @@
+/*
+  $Id: Dsmlv1.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.dsml;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.LdapUtil;
+import edu.vt.middleware.ldap.bean.LdapAttribute;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import org.dom4j.Document;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.Namespace;
+import org.dom4j.QName;
+
+/**
+ * <code>Dsmlv1</code> contains functions for converting LDAP search result sets
+ * into DSML version 1.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class Dsmlv1 extends AbstractDsml
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = 1047858330816575821L;
+
+
+  /** Default constructor. */
+  public Dsmlv1() {}
+
+
+  /**
+   * This will take the results of a prior LDAP query and convert it to a DSML
+   * <code>Document</code>.
+   *
+   * @param  results  <code>Iterator</code> of LDAP search results
+   *
+   * @return  <code>Document</code>
+   */
+  public Document createDsml(final Iterator<SearchResult> results)
+  {
+    Document dsml = null;
+    try {
+      final LdapResult lr = this.beanFactory.newLdapResult();
+      lr.addEntries(results);
+      dsml = this.createDsml(lr);
+    } catch (NamingException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error creating Element from SearchResult", e);
+      }
+    }
+    return dsml;
+  }
+
+
+  /**
+   * This will take the results of a prior LDAP query and convert it to a DSML
+   * <code>Document</code>.
+   *
+   * @param  result  <code>LdapResult</code>
+   *
+   * @return  <code>Document</code>
+   */
+  public Document createDsml(final LdapResult result)
+  {
+    final Namespace ns = new Namespace("dsml", "http://www.dsml.org/DSML");
+    final Document doc = DocumentHelper.createDocument();
+    final Element dsmlElement = doc.addElement(new QName("dsml", ns));
+    final Element entriesElement = dsmlElement.addElement(
+      new QName("directory-entries", ns));
+
+    // build document object from result
+    if (result != null) {
+      for (LdapEntry le : result.getEntries()) {
+        final Element entryElement = this.createDsmlEntry(
+          new QName("entry", ns),
+          le,
+          ns);
+        entriesElement.add(entryElement);
+      }
+    }
+
+    return doc;
+  }
+
+
+  /** {@inheritDoc} */
+  protected List<Element> createDsmlAttributes(
+    final LdapAttributes ldapAttributes,
+    final Namespace ns)
+  {
+    final List<Element> attrElements = new ArrayList<Element>();
+    for (LdapAttribute attr : ldapAttributes.getAttributes()) {
+      final String attrName = attr.getName();
+      final Set<?> attrValues = attr.getValues();
+      Element attrElement = null;
+      if (attrName.equalsIgnoreCase("objectclass")) {
+        attrElement = createDsmlAttribute(
+          attrName,
+          attrValues,
+          ns,
+          "objectclass",
+          null,
+          "oc-value");
+        if (attrElement.hasContent()) {
+          attrElements.add(0, attrElement);
+        }
+      } else {
+        attrElement = createDsmlAttribute(
+          attrName,
+          attrValues,
+          ns,
+          "attr",
+          "name",
+          "value");
+        if (attrElement.hasContent()) {
+          attrElements.add(attrElement);
+        }
+      }
+    }
+    return attrElements;
+  }
+
+
+  /**
+   * This will take a DSML <code>Document</code> and convert it to an Iterator
+   * of LDAP search results.
+   *
+   * @param  doc  <code>Document</code> of DSML
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   */
+  public Iterator<SearchResult> createSearchResults(final Document doc)
+  {
+    return this.createLdapResult(doc).toSearchResults().iterator();
+  }
+
+
+  /**
+   * This will take a DSML <code>Document</code> and convert it to an <code>
+   * LdapResult</code>.
+   *
+   * @param  doc  <code>Document</code> of DSML
+   *
+   * @return  <code>LdapResult</code>
+   */
+  public LdapResult createLdapResult(final Document doc)
+  {
+    final LdapResult result = this.beanFactory.newLdapResult();
+
+    if (doc != null && doc.hasContent()) {
+      final Iterator<?> entryIterator = doc.selectNodes(
+        "/dsml:dsml/dsml:directory-entries/dsml:entry").iterator();
+      while (entryIterator.hasNext()) {
+        final LdapEntry le = this.createLdapEntry(
+          (Element) entryIterator.next());
+        if (result != null) {
+          result.addEntry(le);
+        }
+      }
+    }
+
+    return result;
+  }
+
+
+  /**
+   * This will take a DSML <code>Element</code> containing an entry of type
+   * <dsml:entry name="name"/> and convert it to an LDAP entry.
+   *
+   * @param  entryElement  <code>Element</code> of DSML content
+   *
+   * @return  <code>LdapEntry</code>
+   */
+  protected LdapEntry createLdapEntry(final Element entryElement)
+  {
+    final LdapEntry ldapEntry = this.beanFactory.newLdapEntry();
+    ldapEntry.setDn("");
+
+    if (entryElement != null) {
+
+      final String name = entryElement.attributeValue("dn");
+      if (name != null) {
+        ldapEntry.setDn(name);
+      }
+
+      if (entryElement.hasContent()) {
+
+        final Iterator<?> ocIterator = entryElement.elementIterator(
+          "objectclass");
+        while (ocIterator.hasNext()) {
+          final Element ocElement = (Element) ocIterator.next();
+          if (ocElement != null && ocElement.hasContent()) {
+            final String ocName = "objectClass";
+            final LdapAttribute ldapAttribute = this.beanFactory
+                .newLdapAttribute();
+            ldapAttribute.setName(ocName);
+
+            final Iterator<?> valueIterator = ocElement.elementIterator(
+              "oc-value");
+            while (valueIterator.hasNext()) {
+              final Element valueElement = (Element) valueIterator.next();
+              if (valueElement != null) {
+                final String value = valueElement.getText();
+                if (value != null) {
+                  final String encoding = valueElement.attributeValue(
+                    "encoding");
+                  if (encoding != null && "base64".equals(encoding)) {
+                    ldapAttribute.getValues().add(LdapUtil.base64Decode(value));
+                  } else {
+                    ldapAttribute.getValues().add(value);
+                  }
+                }
+              }
+            }
+            ldapEntry.getLdapAttributes().addAttribute(ldapAttribute);
+          }
+        }
+
+        ldapEntry.getLdapAttributes().addAttributes(
+          super.createLdapEntry(entryElement).getLdapAttributes()
+              .getAttributes());
+      }
+    }
+
+    return ldapEntry;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/dsml/Dsmlv2.java b/src/main/java/edu/vt/middleware/ldap/dsml/Dsmlv2.java
new file mode 100644
index 0000000..184ddfa
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/dsml/Dsmlv2.java
@@ -0,0 +1,148 @@
+/*
+  $Id: Dsmlv2.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.dsml;
+
+import java.util.Iterator;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import org.dom4j.Document;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.Namespace;
+import org.dom4j.QName;
+
+/**
+ * <code>Dsmlv2</code> contains functions for converting LDAP search result sets
+ * into DSML version 2.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class Dsmlv2 extends AbstractDsml
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -1503268164295032020L;
+
+
+  /** Default constructor. */
+  public Dsmlv2() {}
+
+
+  /**
+   * This will take the results of a prior LDAP query and convert it to a DSML
+   * <code>Document</code>.
+   *
+   * @param  results  <code>Iterator</code> of LDAP search results
+   *
+   * @return  <code>Document</code>
+   */
+  public Document createDsml(final Iterator<SearchResult> results)
+  {
+    Document dsml = null;
+    try {
+      final LdapResult lr = this.beanFactory.newLdapResult();
+      lr.addEntries(results);
+      dsml = this.createDsml(lr);
+    } catch (NamingException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error creating Element from SearchResult", e);
+      }
+    }
+    return dsml;
+  }
+
+
+  /**
+   * This will take the results of a prior LDAP query and convert it to a DSML
+   * <code>Document</code>.
+   *
+   * @param  result  <code>LdapResult</code>
+   *
+   * @return  <code>Document</code>
+   */
+  public Document createDsml(final LdapResult result)
+  {
+    final Namespace ns = new Namespace("", "urn:oasis:names:tc:DSML:2:0:core");
+    final Document doc = DocumentHelper.createDocument();
+    final Element dsmlElement = doc.addElement(new QName("batchResponse", ns));
+    final Element entriesElement = dsmlElement.addElement(
+      new QName("searchResponse", ns));
+
+    // build document object from results
+    if (result != null) {
+      for (LdapEntry le : result.getEntries()) {
+        final Element entryElement = this.createDsmlEntry(
+          new QName("searchResultEntry", ns),
+          le,
+          ns);
+        entriesElement.add(entryElement);
+      }
+    }
+
+    final Element doneElement = entriesElement.addElement(
+      new QName("searchResultDone", ns));
+    final Element codeElement = doneElement.addElement(
+      new QName("resultCode", ns));
+    codeElement.addAttribute("code", "0");
+
+    return doc;
+  }
+
+
+  /**
+   * This will take a DSML <code>Document</code> and convert it to an Iterator
+   * of LDAP search results.
+   *
+   * @param  doc  <code>Document</code> of DSML
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   */
+  public Iterator<SearchResult> createSearchResults(final Document doc)
+  {
+    return this.createLdapResult(doc).toSearchResults().iterator();
+  }
+
+
+  /**
+   * This will take a DSML <code>Document</code> and convert it to a <code>
+   * LdapResult</code>.
+   *
+   * @param  doc  <code>Document</code> of DSML
+   *
+   * @return  <code>LdapResult</code>
+   */
+  public LdapResult createLdapResult(final Document doc)
+  {
+    final LdapResult result = this.beanFactory.newLdapResult();
+
+    if (doc != null && doc.hasContent()) {
+      final Iterator<?> entryIterator = doc.selectNodes(
+        "/*[name()='batchResponse']" +
+        "/*[name()='searchResponse']" +
+        "/*[name()='searchResultEntry']").iterator();
+      while (entryIterator.hasNext()) {
+        final LdapEntry le = this.createLdapEntry(
+          (Element) entryIterator.next());
+        if (le != null) {
+          result.addEntry(le);
+        }
+      }
+    }
+
+    return result;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/AbstractConnectionHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/AbstractConnectionHandler.java
new file mode 100644
index 0000000..efd8764
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/AbstractConnectionHandler.java
@@ -0,0 +1,331 @@
+/*
+  $Id: AbstractConnectionHandler.java 2397 2012-05-23 20:05:27Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2397 $
+  Updated: $Date: 2012-05-23 21:05:27 +0100 (Wed, 23 May 2012) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.StringTokenizer;
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.LdapConstants;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractConnectionHandler</code> provides a basic implementation for
+ * other connection handlers to inherit.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2397 $
+ */
+public abstract class AbstractConnectionHandler implements ConnectionHandler
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Ldap configuration. */
+  protected LdapConfig config;
+
+  /** Ldap context. */
+  protected LdapContext context;
+
+  /** Ldap connection strategy. */
+  protected ConnectionStrategy connectionStrategy = ConnectionStrategy.DEFAULT;
+
+  /** Exception types to retry connections on. */
+  protected Class<?>[] connectionRetryExceptions = new Class[] {
+    NamingException.class,
+  };
+
+  /** Number of connections made. */
+  private ConnectionCount connectionCount = new ConnectionCount();
+
+
+  /**
+   * Returns the connection count.
+   *
+   * @return  connection count
+   */
+  protected ConnectionCount getConnectionCount()
+  {
+    return this.connectionCount;
+  }
+
+
+  /**
+   * Sets the connection count.
+   *
+   * @param  cc  connection count
+   */
+  protected void setConnectionCount(final ConnectionCount cc)
+  {
+    this.connectionCount = cc;
+  }
+
+
+  /** {@inheritDoc} */
+  public ConnectionStrategy getConnectionStrategy()
+  {
+    return this.connectionStrategy;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setConnectionStrategy(final ConnectionStrategy strategy)
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting connectionStrategy: " + strategy);
+    }
+    this.connectionStrategy = strategy;
+  }
+
+
+  /** {@inheritDoc} */
+  public Class<?>[] getConnectionRetryExceptions()
+  {
+    return this.connectionRetryExceptions;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setConnectionRetryExceptions(final Class<?>[] exceptions)
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "setting connectionRetryExceptions: " + Arrays.toString(exceptions));
+    }
+    this.connectionRetryExceptions = exceptions;
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapConfig getLdapConfig()
+  {
+    return this.config;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setLdapConfig(final LdapConfig lc)
+  {
+    this.config = lc;
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapContext getLdapContext()
+  {
+    return this.context;
+  }
+
+
+  /** {@inheritDoc} */
+  public void connect(final String dn, final Object credential)
+    throws NamingException
+  {
+    NamingException lastThrown = null;
+    final String[] urls = this.parseLdapUrl(
+      this.config.getLdapUrl(),
+      this.connectionStrategy);
+    for (String url : urls) {
+      final Hashtable<String, Object> env = new Hashtable<String, Object>(
+        this.config.getEnvironment());
+      env.put(LdapConstants.PROVIDER_URL, url);
+      try {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace(
+            "{" + this.connectionCount + "} Attempting connection to " + url +
+            " for strategy " + this.connectionStrategy);
+        }
+        this.connectInternal(this.config.getAuthtype(), dn, credential, env);
+        this.connectionCount.incrementCount();
+        lastThrown = null;
+        break;
+      } catch (NamingException e) {
+        lastThrown = e;
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Error connecting to LDAP URL: " + url, e);
+        }
+
+        boolean ignoreException = false;
+        if (
+          this.connectionRetryExceptions != null &&
+            this.connectionRetryExceptions.length > 0) {
+          for (Class<?> ne : this.connectionRetryExceptions) {
+            if (ne.isInstance(e)) {
+              ignoreException = true;
+              break;
+            }
+          }
+        }
+        if (!ignoreException) {
+          break;
+        }
+      }
+    }
+    if (lastThrown != null) {
+      throw lastThrown;
+    }
+  }
+
+
+  /**
+   * Create the initial ldap context and prepare the connection for use.
+   *
+   * @param  authtype  security mechanism to bind with
+   * @param  dn  to bind as
+   * @param  credential  to bind with in conjunction with dn
+   * @param  env  to pass to the initial ldap context
+   *
+   * @throws  NamingException  if a connection cannot be established
+   */
+  protected abstract void connectInternal(
+    final String authtype,
+    final String dn,
+    final Object credential,
+    final Hashtable<String, Object> env)
+    throws NamingException;
+
+
+  /** {@inheritDoc} */
+  public boolean isConnected()
+  {
+    return this.context != null;
+  }
+
+
+  /** {@inheritDoc} */
+  public void close()
+    throws NamingException
+  {
+    try {
+      if (this.context != null) {
+        this.context.close();
+      }
+    } finally {
+      this.context = null;
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract ConnectionHandler newInstance();
+
+
+  /**
+   * Parses the supplied ldap url and splits it into separate URLs if it is
+   * space delimited.
+   *
+   * @param  ldapUrl  to parse
+   * @param  strategy  of ordered array to return
+   *
+   * @return  array of ldap URLs
+   */
+  protected String[] parseLdapUrl(
+    final String ldapUrl,
+    final ConnectionStrategy strategy)
+  {
+    String[] urls = null;
+    if (strategy == ConnectionStrategy.DEFAULT) {
+      urls = new String[] {ldapUrl};
+    } else if (strategy == ConnectionStrategy.ACTIVE_PASSIVE) {
+      final List<String> l = this.splitLdapUrl(ldapUrl);
+      urls = l.toArray(new String[l.size()]);
+    } else if (strategy == ConnectionStrategy.ROUND_ROBIN) {
+      final List<String> l = this.splitLdapUrl(ldapUrl);
+      for (int i = 0; i < this.connectionCount.getCount() % l.size(); i++) {
+        l.add(l.remove(0));
+      }
+      urls = l.toArray(new String[l.size()]);
+    } else if (strategy == ConnectionStrategy.RANDOM) {
+      final List<String> l = this.splitLdapUrl(ldapUrl);
+      Collections.shuffle(l);
+      urls = l.toArray(new String[l.size()]);
+    }
+    return urls;
+  }
+
+
+  /**
+   * Takes a space delimited string of URLs and returns a list of URLs.
+   *
+   * @param  url  to split
+   *
+   * @return  list of URLs
+   */
+  private List<String> splitLdapUrl(final String url)
+  {
+    final List<String> urls = new ArrayList<String>();
+    if (url != null) {
+      final StringTokenizer st = new StringTokenizer(url);
+      while (st.hasMoreTokens()) {
+        urls.add(st.nextToken());
+      }
+    } else {
+      urls.add(null);
+    }
+    return urls;
+  }
+
+
+  /**
+   * <code>ConnectionCount</code> provides an object to track the connection
+   * count.
+   */
+  private class ConnectionCount
+  {
+
+    /** connection count. */
+    private int count;
+
+
+    /**
+     * Returns the connection count.
+     *
+     * @return  count
+     */
+    public int getCount()
+    {
+      return this.count;
+    }
+
+
+    /** Increments the connection count. */
+    public void incrementCount()
+    {
+      this.count++;
+      // reset the count if it exceeds the size of an integer
+      if (this.count < 0) {
+        this.count = 0;
+      }
+    }
+
+
+    /**
+     * Returns a string representation of this object.
+     *
+     * @return  count as a string
+     */
+    @Override
+    public String toString()
+    {
+      return Integer.toString(this.count);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/AbstractResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/AbstractResultHandler.java
new file mode 100644
index 0000000..1348376
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/AbstractResultHandler.java
@@ -0,0 +1,150 @@
+/*
+  $Id: AbstractResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractResultHandler</code> implements common handler functionality.
+ *
+ * @param  <R>  type of result
+ * @param  <O>  type of output
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class AbstractResultHandler<R, O> implements ResultHandler<R, O>
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+
+  /**
+   * This will enumerate through the supplied <code>NamingEnumeration</code> and
+   * return a List of those results. The results are unaltered and the dn is
+   * ignored.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find enumeration
+   * @param  en  <code>NamingEnumeration</code> LDAP results
+   *
+   * @return  <code>List</code> - LDAP results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public List<O> process(
+    final SearchCriteria sc,
+    final NamingEnumeration<? extends R> en)
+    throws NamingException
+  {
+    return this.process(sc, en, null);
+  }
+
+
+  /**
+   * This will enumerate through the supplied <code>NamingEnumeration</code> and
+   * return a List of those results. The results are unaltered and the dn is
+   * ignored. Any exceptions passed into this method will be ignored and results
+   * will be returned as if no exception occurred.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find enumeration
+   * @param  en  <code>NamingEnumeration</code> LDAP results
+   * @param  ignore  <code>Class[]</code> of exception types to ignore
+   *
+   * @return  <code>List</code> - LDAP results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public List<O> process(
+    final SearchCriteria sc,
+    final NamingEnumeration<? extends R> en,
+    final Class<?>[] ignore)
+    throws NamingException
+  {
+    final List<O> results = new ArrayList<O>();
+    if (en != null) {
+      try {
+        while (en.hasMore()) {
+          final O o = processResult(sc, en.next());
+          if (o != null) {
+            results.add(o);
+          }
+        }
+      } catch (NamingException e) {
+        boolean ignoreException = false;
+        if (ignore != null && ignore.length > 0) {
+          for (Class<?> ne : ignore) {
+            if (ne.isInstance(e)) {
+              if (this.logger.isDebugEnabled()) {
+                this.logger.debug("Ignoring naming exception", e);
+              }
+              ignoreException = true;
+              break;
+            }
+          }
+        }
+        if (!ignoreException) {
+          throw e;
+        }
+      }
+    }
+    return results;
+  }
+
+
+  /**
+   * This will enumerate through the supplied <code>List</code> and return a
+   * List of those results. The results are unaltered and the dn is ignored.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find enumeration
+   * @param  l  <code>List</code> LDAP results
+   *
+   * @return  <code>List</code> - LDAP results
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public List<O> process(final SearchCriteria sc, final List<? extends R> l)
+    throws NamingException
+  {
+    final List<O> results = new ArrayList<O>();
+    if (l != null) {
+      for (R r : l) {
+        final O o = processResult(sc, r);
+        if (o != null) {
+          results.add(o);
+        }
+      }
+    }
+    return results;
+  }
+
+
+  /**
+   * Processes the supplied result.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to retrieve the result
+   * @param  r  <code>R</code> result to process
+   *
+   * @return  <code>O</code> processed result
+   *
+   * @throws  NamingException  if the supplied result cannot be read
+   */
+  protected abstract O processResult(final SearchCriteria sc, final R r)
+    throws NamingException;
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/AttributeHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/AttributeHandler.java
new file mode 100644
index 0000000..5fb740d
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/AttributeHandler.java
@@ -0,0 +1,24 @@
+/*
+  $Id: AttributeHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.directory.Attribute;
+
+/**
+ * AttributeHandler provides post search processing of an ldap attribute.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public interface AttributeHandler extends ResultHandler<Attribute, Attribute> {}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/AttributesProcessor.java b/src/main/java/edu/vt/middleware/ldap/handler/AttributesProcessor.java
new file mode 100644
index 0000000..1da47e7
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/AttributesProcessor.java
@@ -0,0 +1,89 @@
+/*
+  $Id: AttributesProcessor.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+
+/**
+ * <code>AttributesProcessor</code> provides methods to help with the processing
+ * of Attributes objects using an AttributeHandler.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class AttributesProcessor
+{
+
+
+  /** Default constructor. */
+  private AttributesProcessor() {}
+
+
+  /**
+   * Process the attributes of an ldap search search.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find search result
+   * @param  attrs  <code>Attributes</code> to pass to the handler
+   * @param  handler  <code>AttributeHandler</code> to process attributes
+   *
+   * @return  <code>Attributes</code> handler processed attributes
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public static Attributes executeHandler(
+    final SearchCriteria sc,
+    final Attributes attrs,
+    final AttributeHandler handler)
+    throws NamingException
+  {
+    return executeHandler(sc, attrs, handler, null);
+  }
+
+
+  /**
+   * Process the attributes of an ldap search search. Any exceptions passed into
+   * this method will be ignored and results will be returned as if no exception
+   * occurred.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find search result
+   * @param  attrs  <code>Attributes</code> to pass to the handler
+   * @param  handler  <code>AttributeHandler</code> to process attributes
+   * @param  ignore  <code>Class[]</code> of exception types to ignore
+   *
+   * @return  <code>Attributes</code> handler processed attributes
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  public static Attributes executeHandler(
+    final SearchCriteria sc,
+    final Attributes attrs,
+    final AttributeHandler handler,
+    final Class<?>[] ignore)
+    throws NamingException
+  {
+    Attributes newAttrs = null;
+    if (handler != null) {
+      newAttrs = new BasicAttributes(attrs.isCaseIgnored());
+      for (Attribute attr : handler.process(sc, attrs.getAll(), ignore)) {
+        newAttrs.put(attr);
+      }
+    } else {
+      newAttrs = attrs;
+    }
+    return newAttrs;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/BinaryAttributeHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/BinaryAttributeHandler.java
new file mode 100644
index 0000000..6de2159
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/BinaryAttributeHandler.java
@@ -0,0 +1,45 @@
+/*
+  $Id: BinaryAttributeHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import edu.vt.middleware.ldap.LdapUtil;
+
+/**
+ * <code>BinaryAttributeHandler</code> ensures that any attribute that contains
+ * a value of type byte[] is base64 encoded.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class BinaryAttributeHandler extends CopyAttributeHandler
+{
+
+
+  /**
+   * This base64 encodes the supplied value if it is of type byte[].
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find enumeration
+   * @param  value  <code>Object</code> to process
+   *
+   * @return  <code>Object</code>
+   */
+  protected Object processValue(final SearchCriteria sc, final Object value)
+  {
+    if (value instanceof byte[]) {
+      return LdapUtil.base64Encode((byte[]) value);
+    } else {
+      return value;
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/BinarySearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/BinarySearchResultHandler.java
new file mode 100644
index 0000000..61b2f39
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/BinarySearchResultHandler.java
@@ -0,0 +1,33 @@
+/*
+  $Id: BinarySearchResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+/**
+ * <code>BinarySearchResultHandler</code> provides a search result handler which
+ * uses {@link BinaryAttributeHandler}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class BinarySearchResultHandler extends CopySearchResultHandler
+{
+
+
+  /** Creates a new <code>BinarySearchResultHandler</code>. */
+  public BinarySearchResultHandler()
+  {
+    this.setAttributeHandler(
+      new AttributeHandler[] {new BinaryAttributeHandler()});
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/CaseChangeAttributeHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/CaseChangeAttributeHandler.java
new file mode 100644
index 0000000..6a52604
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/CaseChangeAttributeHandler.java
@@ -0,0 +1,113 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.BasicAttribute;
+import edu.vt.middleware.ldap.handler.CaseChangeSearchResultHandler.CaseChange;
+
+/**
+ * <code>CaseChangeAttributeHandler</code> provides the ability to modify the
+ * case of attribute names and attribute values.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 18:10:53 -0400 (Sun, 23 May 2010) $
+ */
+public class CaseChangeAttributeHandler extends CopyAttributeHandler
+{
+
+  /** Type of case modification to make to the attribute names. */
+  private CaseChange attributeNameCaseChange = CaseChange.NONE;
+
+  /** Type of case modification to make to the attributes values. */
+  private CaseChange attributeValueCaseChange = CaseChange.NONE;
+
+
+  /**
+   * Returns the attribute name case change.
+   *
+   * @return  <code>CaseChange</code>
+   */
+  public CaseChange getAttributeNameCaseChange()
+  {
+    return this.attributeNameCaseChange;
+  }
+
+
+  /**
+   * Sets the attribute name case change.
+   *
+   * @param  caseChange  <code>CaseChange</code>
+   */
+  public void setAttributeNameCaseChange(final CaseChange caseChange)
+  {
+    this.attributeNameCaseChange = caseChange;
+  }
+
+
+  /**
+   * Returns the attribute value case change.
+   *
+   * @return  <code>CaseChange</code>
+   */
+  public CaseChange getAttributeValueCaseChange()
+  {
+    return this.attributeValueCaseChange;
+  }
+
+
+  /**
+   * Sets the attribute value case change.
+   *
+   * @param  caseChange  <code>CaseChange</code>
+   */
+  public void setAttributeValueCaseChange(final CaseChange caseChange)
+  {
+    this.attributeValueCaseChange = caseChange;
+  }
+
+
+  /** {@inheritDoc} */
+  protected Attribute processResult(
+    final SearchCriteria sc,
+    final Attribute attr)
+    throws NamingException
+  {
+    Attribute newAttr = null;
+    if (attr != null) {
+      newAttr = new BasicAttribute(
+        CaseChange.perform(this.attributeNameCaseChange, attr.getID()),
+        attr.isOrdered());
+
+      final NamingEnumeration<?> en = attr.getAll();
+      while (en.hasMore()) {
+        newAttr.add(this.processValue(sc, en.next()));
+      }
+    }
+    return newAttr;
+  }
+
+
+  /** {@inheritDoc} */
+  protected Object processValue(final SearchCriteria sc, final Object value)
+  {
+    if (value instanceof String) {
+      return CaseChange.perform(this.attributeValueCaseChange, (String) value);
+    } else {
+      return value;
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/CaseChangeSearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/CaseChangeSearchResultHandler.java
new file mode 100644
index 0000000..3553a8f
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/CaseChangeSearchResultHandler.java
@@ -0,0 +1,150 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>CaseSearchResultHandler</code> provides the ability to modify the case
+ * of ldap search result DNs, attribute names, and attribute values.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 18:10:53 -0400 (Sun, 23 May 2010) $
+ */
+public class CaseChangeSearchResultHandler extends CopySearchResultHandler
+{
+
+  /** Enum to define the type of case change. */
+  public enum CaseChange {
+
+    /** no case change. */
+    NONE,
+
+    /** lower case. */
+    LOWER,
+
+    /** upper case. */
+    UPPER;
+
+
+    /**
+     * This changes the supplied string based on the supplied case change.
+     *
+     * @param  cc  <code>CaseChange</code> to perform
+     * @param  string  <code>String</code> to modify
+     *
+     * @return  <code>String</code> that has been changed
+     */
+    public static String perform(final CaseChange cc, final String string)
+    {
+      String s = null;
+      if (CaseChange.LOWER == cc) {
+        s = string.toLowerCase();
+      } else if (CaseChange.UPPER == cc) {
+        s = string.toUpperCase();
+      } else if (CaseChange.NONE == cc) {
+        s = string;
+      }
+      return s;
+    }
+  }
+
+
+  /** Type of case modification to make to the entry DN. */
+  private CaseChange dnCaseChange = CaseChange.NONE;
+
+  /** Attribute handler for case modifications. */
+  private CaseChangeAttributeHandler attributeHandler =
+    new CaseChangeAttributeHandler();
+
+
+  /** Creates a new <code>CaseSearchResultHandler</code>. */
+  public CaseChangeSearchResultHandler()
+  {
+    this.setAttributeHandler(new AttributeHandler[] {this.attributeHandler});
+  }
+
+
+  /**
+   * Returns the DN case change.
+   *
+   * @return  <code>CaseChange</code>
+   */
+  public CaseChange getDnCaseChange()
+  {
+    return this.dnCaseChange;
+  }
+
+
+  /**
+   * Sets the DN case change.
+   *
+   * @param  caseChange  <code>CaseChange</code>
+   */
+  public void setDnCaseChange(final CaseChange caseChange)
+  {
+    this.dnCaseChange = caseChange;
+  }
+
+
+  /**
+   * Returns the attribute name case change.
+   *
+   * @return  <code>CaseChange</code>
+   */
+  public CaseChange getAttributeNameCaseChange()
+  {
+    return this.attributeHandler.getAttributeNameCaseChange();
+  }
+
+
+  /**
+   * Sets the attribute name case change.
+   *
+   * @param  caseChange  <code>CaseChange</code>
+   */
+  public void setAttributeNameCaseChange(final CaseChange caseChange)
+  {
+    this.attributeHandler.setAttributeNameCaseChange(caseChange);
+  }
+
+
+  /**
+   * Returns the attribute value case change.
+   *
+   * @return  <code>CaseChange</code>
+   */
+  public CaseChange getAttributeValueCaseChange()
+  {
+    return this.attributeHandler.getAttributeValueCaseChange();
+  }
+
+
+  /**
+   * Sets the attribute value case change.
+   *
+   * @param  caseChange  <code>CaseChange</code>
+   */
+  public void setAttributeValueCaseChange(final CaseChange caseChange)
+  {
+    this.attributeHandler.setAttributeValueCaseChange(caseChange);
+  }
+
+
+  /** {@inheritDoc} */
+  protected String processDn(final SearchCriteria sc, final SearchResult sr)
+  {
+    return CaseChange.perform(this.dnCaseChange, sr.getName());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/ConnectionHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/ConnectionHandler.java
new file mode 100644
index 0000000..e2f4876
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/ConnectionHandler.java
@@ -0,0 +1,143 @@
+/*
+  $Id: ConnectionHandler.java 1616 2010-09-21 17:22:27Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1616 $
+  Updated: $Date: 2010-09-21 18:22:27 +0100 (Tue, 21 Sep 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+import edu.vt.middleware.ldap.LdapConfig;
+
+/**
+ * ConnectionHandler provides an interface for creating and closing LDAP
+ * connections.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1616 $
+ */
+public interface ConnectionHandler
+{
+
+  /** Enum to define the type of connection strategy. */
+  public enum ConnectionStrategy {
+
+    /** default strategy. */
+    DEFAULT,
+
+    /** active-passive strategy. */
+    ACTIVE_PASSIVE,
+
+    /** round robin strategy. */
+    ROUND_ROBIN,
+
+    /** random strategy. */
+    RANDOM,
+  }
+
+
+  /**
+   * Returns the connection strategy.
+   *
+   * @return  strategy for making connections
+   */
+  ConnectionStrategy getConnectionStrategy();
+
+
+  /**
+   * Sets the connection strategy.
+   *
+   * @param  strategy  for making connections
+   */
+  void setConnectionStrategy(ConnectionStrategy strategy);
+
+
+  /**
+   * This returns the exception types to retry connections on.
+   *
+   * @return  <code>Class[]</code>
+   */
+  Class<?>[] getConnectionRetryExceptions();
+
+
+  /**
+   * This sets the exception types to retry connections on.
+   *
+   * @param  exceptions  <code>Class[]</code>
+   */
+  void setConnectionRetryExceptions(Class<?>[] exceptions);
+
+
+  /**
+   * Returns the ldap configuration.
+   *
+   * @return  ldap config
+   */
+  LdapConfig getLdapConfig();
+
+
+  /**
+   * Sets the ldap configuration.
+   *
+   * @param  lc  ldap config
+   */
+  void setLdapConfig(LdapConfig lc);
+
+
+  /**
+   * Open a connection to an LDAP.
+   *
+   * @param  dn  to attempt bind with
+   * @param  credential  to attempt bind with
+   *
+   * @throws  NamingException  if an LDAP error occurs
+   */
+  void connect(String dn, Object credential)
+    throws NamingException;
+
+
+  /**
+   * Returns whether the underlying context has been established.
+   *
+   * @return  whether a connection has been made
+   */
+  boolean isConnected();
+
+
+  /**
+   * Returns an ldap context to use for ldap operations. {@link #connect(String,
+   * Object)} must be called prior to invoking this.
+   *
+   * @return  ldap context
+   *
+   * @throws  NamingException  if an LDAP error occurs
+   */
+  LdapContext getLdapContext()
+    throws NamingException;
+
+
+  /**
+   * Close a connection to an LDAP.
+   *
+   * @throws  NamingException  if an LDAP error occurs
+   */
+  void close()
+    throws NamingException;
+
+
+  /**
+   * Returns a separate instance of this connection handler with the same
+   * underlying ldap configuration.
+   *
+   * @return  connection handler
+   */
+  ConnectionHandler newInstance();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/CopyAttributeHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/CopyAttributeHandler.java
new file mode 100644
index 0000000..e935163
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/CopyAttributeHandler.java
@@ -0,0 +1,73 @@
+/*
+  $Id: CopyAttributeHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.BasicAttribute;
+
+/**
+ * <code>CopyAttributeHandler</code> converts a NamingEnumeration of attribute
+ * into a List of attribute.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class CopyAttributeHandler extends CopyResultHandler<Attribute>
+  implements AttributeHandler
+{
+
+
+  /**
+   * This will return a deep copy of the supplied <code>Attribute</code>.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find enumeration
+   * @param  attr  <code>Attribute</code> to copy
+   *
+   * @return  <code>Attribute</code>
+   *
+   * @throws  NamingException  if the attribute values cannot be read
+   */
+  protected Attribute processResult(
+    final SearchCriteria sc,
+    final Attribute attr)
+    throws NamingException
+  {
+    Attribute newAttr = null;
+    if (attr != null) {
+      newAttr = new BasicAttribute(attr.getID(), attr.isOrdered());
+
+      final NamingEnumeration<?> en = attr.getAll();
+      while (en.hasMore()) {
+        newAttr.add(this.processValue(sc, en.next()));
+      }
+    }
+    return newAttr;
+  }
+
+
+  /**
+   * This returns the supplied value unaltered.
+   *
+   * @param  sc  <code>LdapSearchCritieria</code> used to find enumeration
+   * @param  value  <code>Object</code> to process
+   *
+   * @return  <code>Object</code>
+   */
+  protected Object processValue(final SearchCriteria sc, final Object value)
+  {
+    return value;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/CopyResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/CopyResultHandler.java
new file mode 100644
index 0000000..6c4826c
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/CopyResultHandler.java
@@ -0,0 +1,46 @@
+/*
+  $Id: CopyResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.NamingException;
+
+/**
+ * <code>CopyResultHandler</code> converts a NamingEnumeration into a List of
+ * ldap results.
+ *
+ * @param  <T>  type of result
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class CopyResultHandler<T> extends AbstractResultHandler<T, T>
+{
+
+
+  /**
+   * Returns the supplied result unaltered.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to retrieve the result
+   * @param  r  <code>T</code> to process
+   *
+   * @return  <code>T</code> result that was supplied
+   *
+   * @throws  NamingException  if the supplied result cannot be read
+   */
+  protected T processResult(final SearchCriteria sc, final T r)
+    throws NamingException
+  {
+    return r;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/CopySearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/CopySearchResultHandler.java
new file mode 100644
index 0000000..2c5b29c
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/CopySearchResultHandler.java
@@ -0,0 +1,111 @@
+/*
+  $Id: CopySearchResultHandler.java 1786 2011-01-05 14:45:07Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1786 $
+  Updated: $Date: 2011-01-05 14:45:07 +0000 (Wed, 05 Jan 2011) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>CopySearchResultHandler</code> converts a NamingEnumeration of search
+ * results into a List of search results.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1786 $ $Date: 2011-01-05 14:45:07 +0000 (Wed, 05 Jan 2011) $
+ */
+public class CopySearchResultHandler extends CopyResultHandler<SearchResult>
+  implements SearchResultHandler
+{
+
+  /** Attribute handler. */
+  private AttributeHandler[] attributeHandler;
+
+
+  /** {@inheritDoc} */
+  public AttributeHandler[] getAttributeHandler()
+  {
+    return this.attributeHandler;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setAttributeHandler(final AttributeHandler[] ah)
+  {
+    this.attributeHandler = ah;
+  }
+
+
+  /**
+   * This will return a deep copy of the supplied <code>SearchResult</code>.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find enumeration
+   * @param  sr  <code>SearchResult</code> to copy
+   *
+   * @return  <code>SearchResult</code>
+   *
+   * @throws  NamingException  if the result cannot be read
+   */
+  protected SearchResult processResult(
+    final SearchCriteria sc,
+    final SearchResult sr)
+    throws NamingException
+  {
+    return
+      new SearchResult(
+        this.processDn(sc, sr),
+        sr.getClassName(),
+        sr.getObject(),
+        this.processAttributes(sc, sr),
+        sr.isRelative());
+  }
+
+
+  /**
+   * Process the dn of an ldap search result.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find search result
+   * @param  sr  <code>SearchResult</code> to extract the dn from
+   *
+   * @return  <code>String</code> processed dn
+   */
+  protected String processDn(final SearchCriteria sc, final SearchResult sr)
+  {
+    return sr.getName();
+  }
+
+
+  /**
+   * Process the attributes of an ldap search.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to find search result
+   * @param  sr  <code>SearchResult</code> to extract the attributes from
+   *
+   * @return  <code>Attributes</code> processed attributes
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  protected Attributes processAttributes(
+    final SearchCriteria sc,
+    final SearchResult sr)
+    throws NamingException
+  {
+    Attributes newAttrs = sr.getAttributes();
+    if (this.attributeHandler != null && this.attributeHandler.length > 0) {
+      for (AttributeHandler ah : this.attributeHandler) {
+        newAttrs = AttributesProcessor.executeHandler(sc, newAttrs, ah);
+      }
+    }
+    return newAttrs;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/DefaultConnectionHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/DefaultConnectionHandler.java
new file mode 100644
index 0000000..d9ebc95
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/DefaultConnectionHandler.java
@@ -0,0 +1,153 @@
+/*
+  $Id: DefaultConnectionHandler.java 2231 2012-02-02 15:46:27Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2231 $
+  Updated: $Date: 2012-02-02 15:46:27 +0000 (Thu, 02 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.util.Hashtable;
+import javax.naming.NamingException;
+import javax.naming.ldap.InitialLdapContext;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.LdapConstants;
+import edu.vt.middleware.ldap.ssl.ThreadLocalTLSSocketFactory;
+
+/**
+ * <code>DefaultConnectionHandler</code> creates a new <code>LdapContext</code>
+ * using environment properties obtained from {@link
+ * LdapConfig#getEnvironment()}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2231 $
+ */
+public class DefaultConnectionHandler extends AbstractConnectionHandler
+{
+
+
+  /** Default constructor. */
+  public DefaultConnectionHandler() {}
+
+
+  /**
+   * Creates a new <code>DefaultConnectionHandler</code> with the supplied ldap
+   * config.
+   *
+   * @param  lc  ldap config
+   */
+  public DefaultConnectionHandler(final LdapConfig lc)
+  {
+    this.setLdapConfig(lc);
+  }
+
+
+  /**
+   * Copy constructor for <code>DefaultConnectionHandler</code>.
+   *
+   * @param  ch  to copy properties from
+   */
+  public DefaultConnectionHandler(final DefaultConnectionHandler ch)
+  {
+    this.setLdapConfig(ch.getLdapConfig());
+    this.setConnectionStrategy(ch.getConnectionStrategy());
+    this.setConnectionRetryExceptions(ch.getConnectionRetryExceptions());
+    this.setConnectionCount(ch.getConnectionCount());
+  }
+
+
+  /** {@inheritDoc} */
+  protected void connectInternal(
+    final String authtype,
+    final String dn,
+    final Object credential,
+    final Hashtable<String, Object> env)
+    throws NamingException
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Bind with the following parameters:");
+      this.logger.debug("  authtype = " + authtype);
+      this.logger.debug("  dn = " + dn);
+      if (this.config.getLogCredentials()) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("  credential = " + credential);
+        }
+      } else {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("  credential = <suppressed>");
+        }
+      }
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("  env = " + env);
+      }
+    }
+
+    // note that when using simple authentication (the default),
+    // if the credential is null the provider will automatically revert the
+    // authentication to none
+    env.put(LdapConstants.AUTHENTICATION, authtype);
+    if (dn != null) {
+      env.put(LdapConstants.PRINCIPAL, dn);
+      if (credential != null) {
+        env.put(LdapConstants.CREDENTIALS, credential);
+      }
+    }
+
+    // JNDI does not perform hostname validation for LDAPS
+    // set a socket factory that will
+    if (LdapConstants.SSL_PROTOCOL.equals(env.get(LdapConstants.PROTOCOL)) ||
+        ((String) env.get(LdapConstants.PROVIDER_URL)).toLowerCase().contains(
+          "ldaps://")) {
+      if (env.get(LdapConstants.SOCKET_FACTORY) == null) {
+        // parse hostnames for validation
+        final String[] hostnames =
+          ((String) env.get(LdapConstants.PROVIDER_URL)).split(" ");
+        for (int i = 0; i < hostnames.length; i++) {
+          // remove scheme, if it exists
+          if (hostnames[i].startsWith("ldap://")) {
+            hostnames[i] = hostnames[i].substring("ldap://".length());
+          } else if (hostnames[i].startsWith("ldaps://")) {
+            hostnames[i] = hostnames[i].substring("ldaps://".length());
+          }
+          // remove port, if it exist
+          if (hostnames[i].indexOf(":") != -1) {
+            hostnames[i] = hostnames[i].substring(0, hostnames[i].indexOf(":"));
+          }
+        }
+        ThreadLocalTLSSocketFactory.getHostnameVerifierFactory(hostnames);
+        env.put(
+          LdapConstants.SOCKET_FACTORY,
+          ThreadLocalTLSSocketFactory.class.getName());
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Set hostname verifier for ldaps");
+        }
+      }
+    }
+
+    try {
+      this.context = new InitialLdapContext(env, null);
+    } catch (NamingException e) {
+      if (this.context != null) {
+        try {
+          this.context.close();
+        } finally {
+          this.context = null;
+        }
+      }
+      throw e;
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public DefaultConnectionHandler newInstance()
+  {
+    return new DefaultConnectionHandler(this);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/EntryDnSearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/EntryDnSearchResultHandler.java
new file mode 100644
index 0000000..40125ec
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/EntryDnSearchResultHandler.java
@@ -0,0 +1,101 @@
+/*
+  $Id: EntryDnSearchResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>EntryDnSearchResultHandler</code> adds the search result DN as an
+ * attribute to the result set. Provides a client side implementation of RFC
+ * 5020.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class EntryDnSearchResultHandler extends CopySearchResultHandler
+{
+
+  /**
+   * Attribute name for the entry dn. The value of this constant is {@value}.
+   */
+  private String dnAttributeName = "entryDN";
+
+  /**
+   * Whether to add the entry dn if an attribute of the same name exists. The
+   * value of this constant is {@value}.
+   */
+  private boolean addIfExists;
+
+
+  /**
+   * Returns the DN attribute name.
+   *
+   * @return  <code>String</code>
+   */
+  public String getDnAttributeName()
+  {
+    return this.dnAttributeName;
+  }
+
+
+  /**
+   * Sets the DN attribute name.
+   *
+   * @param  s  <code>String</code>
+   */
+  public void setDnAttributeName(final String s)
+  {
+    this.dnAttributeName = s;
+  }
+
+
+  /**
+   * Returns whether to add the entryDN if an attribute of the same name exists.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isAddIfExists()
+  {
+    return this.addIfExists;
+  }
+
+
+  /**
+   * Sets whether to add the entryDN if an attribute of the same name exists.
+   *
+   * @param  b  <code>boolean</code>
+   */
+  public void setAddIfExists(final boolean b)
+  {
+    this.addIfExists = b;
+  }
+
+
+  /** {@inheritDoc} */
+  protected Attributes processAttributes(
+    final SearchCriteria sc,
+    final SearchResult sr)
+    throws NamingException
+  {
+    final Attributes newAttrs = sr.getAttributes();
+    if (newAttrs.get(this.dnAttributeName) == null) {
+      newAttrs.put(this.dnAttributeName, sr.getName());
+    } else if (this.addIfExists) {
+      newAttrs.get(this.dnAttributeName).add(sr.getName());
+    }
+    return newAttrs;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/ExtendedAttributeHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/ExtendedAttributeHandler.java
new file mode 100644
index 0000000..98d6906
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/ExtendedAttributeHandler.java
@@ -0,0 +1,45 @@
+/*
+  $Id: ExtendedAttributeHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import edu.vt.middleware.ldap.Ldap;
+
+/**
+ * Provides an interface for attribute handlers that require the use of the
+ * <code>Ldap</code> object that was used to perform the original search.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public interface ExtendedAttributeHandler extends AttributeHandler
+{
+
+
+  /**
+   * Gets the <code>Ldap</code> used by the search operation invoking this
+   * handler.
+   *
+   * @return  <code>Ldap</code>
+   */
+  Ldap getSearchResultLdap();
+
+
+  /**
+   * Sets the <code>Ldap</code> used by the search operation invoking this
+   * handler.
+   *
+   * @param  l  <code>Ldap</code>
+   */
+  void setSearchResultLdap(final Ldap l);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/ExtendedSearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/ExtendedSearchResultHandler.java
new file mode 100644
index 0000000..9d1982f
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/ExtendedSearchResultHandler.java
@@ -0,0 +1,45 @@
+/*
+  $Id: ExtendedSearchResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import edu.vt.middleware.ldap.Ldap;
+
+/**
+ * Provides an interface for search result handlers that require the use of the
+ * <code>Ldap</code> object that was used to perform the original search.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public interface ExtendedSearchResultHandler extends SearchResultHandler
+{
+
+
+  /**
+   * Gets the <code>Ldap</code> used by the search operation invoking this
+   * handler.
+   *
+   * @return  <code>Ldap</code>
+   */
+  Ldap getSearchResultLdap();
+
+
+  /**
+   * Sets the <code>Ldap</code> used by the search operation invoking this
+   * handler.
+   *
+   * @param  l  <code>Ldap</code>
+   */
+  void setSearchResultLdap(final Ldap l);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/FqdnSearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/FqdnSearchResultHandler.java
new file mode 100644
index 0000000..b98eb06
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/FqdnSearchResultHandler.java
@@ -0,0 +1,126 @@
+/*
+  $Id: FqdnSearchResultHandler.java 2023 2011-07-11 14:50:38Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2023 $
+  Updated: $Date: 2011-07-11 15:50:38 +0100 (Mon, 11 Jul 2011) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.net.URI;
+import javax.naming.CompositeName;
+import javax.naming.InvalidNameException;
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>FqdnSearchResultHandler</code> ensures that the DN of a search result
+ * is fully qualified. Any non-relative names will have the URL removed if
+ * {@link #getRemoveUrls()} is true.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2023 $ $Date: 2011-07-11 15:50:38 +0100 (Mon, 11 Jul 2011) $
+ */
+public class FqdnSearchResultHandler extends CopySearchResultHandler
+{
+
+  /** Whether to remove the URL from any DNs which are not relative. */
+  private boolean removeUrls = true;
+
+
+  /**
+   * Returns whether the URL will be removed from any DNs which are not
+   * relative. The default value is true.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean getRemoveUrls()
+  {
+    return this.removeUrls;
+  }
+
+
+  /**
+   * Sets whether the URL will be removed from any DNs which are not relative
+   * The default value is true.
+   *
+   * @param  b  <code>boolean</code>
+   */
+  public void setRemoveUrls(final boolean b)
+  {
+    this.removeUrls = b;
+  }
+
+
+  /** {@inheritDoc} */
+  protected String processDn(final SearchCriteria sc, final SearchResult sr)
+  {
+    String newDn = null;
+    final String resultName = sr.getName();
+    if (resultName != null) {
+      StringBuffer fqName = null;
+      if (sr.isRelative()) {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("processing relative dn: " + resultName);
+        }
+        if (sc.getDn() != null) {
+          if (!"".equals(resultName)) {
+            fqName = new StringBuffer(
+              readCompositeName(resultName)).append(",").append(sc.getDn());
+          } else {
+            fqName = new StringBuffer(sc.getDn());
+          }
+        } else {
+          fqName = new StringBuffer(readCompositeName(resultName));
+        }
+      } else {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("processing non-relative dn: " + resultName);
+        }
+        if (this.removeUrls) {
+          fqName = new StringBuffer(
+            readCompositeName(URI.create(resultName).getPath().substring(1)));
+        } else {
+          fqName = new StringBuffer(readCompositeName(resultName));
+        }
+      }
+      newDn = fqName.toString();
+    }
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("processed dn: " + newDn);
+    }
+    return newDn;
+  }
+
+
+  /**
+   * Uses a <code>CompositeName</code> to parse the supplied string.
+   *
+   * @param  s  <code>String</code> composite name to read
+   *
+   * @return  <code>String</code> ldap name
+   */
+  private String readCompositeName(final String s)
+  {
+    final StringBuffer name = new StringBuffer();
+    try {
+      final CompositeName cName = new CompositeName(s);
+      for (int i = 0; i < cName.size(); i++) {
+        name.append(cName.get(i));
+        if (i + 1 < cName.size()) {
+          name.append("/");
+        }
+      }
+    } catch (InvalidNameException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error formatting name: " + s, e);
+      }
+    }
+    return name.toString();
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/MergeSearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/MergeSearchResultHandler.java
new file mode 100644
index 0000000..b7a5b55
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/MergeSearchResultHandler.java
@@ -0,0 +1,137 @@
+/*
+  $Id: MergeSearchResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.SearchResult;
+
+/**
+ * <code>MergeSearchResultHandler</code> merges the attributes found in each
+ * search result into the first search result.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class MergeSearchResultHandler extends CopySearchResultHandler
+{
+
+  /** Whether to allow duplicate attribute values. */
+  private boolean allowDuplicates;
+
+
+  /**
+   * Returns whether to allow duplicate attribute values.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean getAllowDuplicates()
+  {
+    return this.allowDuplicates;
+  }
+
+
+  /**
+   * Sets whether to allow duplicate attribute values.
+   *
+   * @param  b  <code>boolean</code>
+   */
+  public void setAllowDuplicates(final boolean b)
+  {
+    this.allowDuplicates = b;
+  }
+
+
+  /** {@inheritDoc} */
+  public List<SearchResult> process(
+    final SearchCriteria sc,
+    final NamingEnumeration<? extends SearchResult> en,
+    final Class<?>[] ignore)
+    throws NamingException
+  {
+    return this.mergeResults(super.process(sc, en, ignore));
+  }
+
+
+  /** {@inheritDoc} */
+  public List<SearchResult> process(
+    final SearchCriteria sc,
+    final List<? extends SearchResult> l)
+    throws NamingException
+  {
+    return this.mergeResults(super.process(sc, l));
+  }
+
+
+  /**
+   * Merges the search results in the supplied list into a single search result.
+   * This method always returns a list of size zero or one.
+   *
+   * @param  results  <code>List</code> of search results to merge
+   *
+   * @return  <code>List</code> of merged search results
+   *
+   * @throws  NamingException  if an error occurs reading attribute values
+   */
+  protected List<SearchResult> mergeResults(final List<SearchResult> results)
+    throws NamingException
+  {
+    final List<SearchResult> mergedResults = new ArrayList<SearchResult>();
+    SearchResult mergedResult = null;
+    for (SearchResult sr : results) {
+      if (mergedResult == null) {
+        mergedResult = sr;
+      } else {
+        final NamingEnumeration<? extends Attribute> en = sr.getAttributes()
+            .getAll();
+        while (en.hasMore()) {
+          final Attribute newAttr = en.next();
+          final Attribute oldAttr = mergedResult.getAttributes().get(
+            newAttr.getID());
+          if (oldAttr == null) {
+            mergedResult.getAttributes().put(newAttr);
+          } else {
+            final NamingEnumeration<?> newValues = newAttr.getAll();
+            while (newValues.hasMore()) {
+              final Object newValue = newValues.next();
+              if (this.allowDuplicates) {
+                oldAttr.add(newValue);
+              } else {
+                boolean add = true;
+                final NamingEnumeration<?> existingValues = oldAttr.getAll();
+                while (existingValues.hasMore()) {
+                  final Object existingValue = existingValues.next();
+                  if (existingValue.equals(newValue)) {
+                    add = false;
+                    break;
+                  }
+                }
+                if (add) {
+                  oldAttr.add(newValue);
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    if (mergedResult != null) {
+      mergedResults.add(mergedResult);
+    }
+    return mergedResults;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/RecursiveAttributeHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/RecursiveAttributeHandler.java
new file mode 100644
index 0000000..6731bdc
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/RecursiveAttributeHandler.java
@@ -0,0 +1,187 @@
+/*
+  $Id: RecursiveAttributeHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import edu.vt.middleware.ldap.Ldap;
+
+/**
+ * <code>RecursiveAttributeHandler</code> will recursively search for attributes
+ * of the same name and combine them into one attribute. Attribute values must
+ * represent DNs in the LDAP.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class RecursiveAttributeHandler extends CopyAttributeHandler
+  implements ExtendedAttributeHandler
+{
+
+  /** Ldap to use for searching. */
+  private Ldap ldap;
+
+  /** Attribute name to search for. */
+  private String attributeName;
+
+
+  /**
+   * Creates a new <code>RecursiveAttributeHandler</code> with the supplied
+   * attribute name.
+   *
+   * @param  attrName  <code>String</code>
+   */
+  public RecursiveAttributeHandler(final String attrName)
+  {
+    this(null, attrName);
+  }
+
+
+  /**
+   * Creates a new <code>RecursiveAttributeHandler</code> with the supplied ldap
+   * and attribute name.
+   *
+   * @param  l  <code>Ldap</code>
+   * @param  attrName  <code>String</code>
+   */
+  public RecursiveAttributeHandler(final Ldap l, final String attrName)
+  {
+    this.ldap = l;
+    this.attributeName = attrName;
+  }
+
+
+  /** {@inheritDoc} */
+  public Ldap getSearchResultLdap()
+  {
+    return this.ldap;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setSearchResultLdap(final Ldap l)
+  {
+    this.ldap = l;
+  }
+
+
+  /**
+   * Returns the attribute name that will be recursively searched on.
+   *
+   * @return  <code>String</code> attribute name
+   */
+  public String getAttributeName()
+  {
+    return this.attributeName;
+  }
+
+
+  /**
+   * Sets the attribute name that will be recursively searched on.
+   *
+   * @param  s  <code>String</code>
+   */
+  public void setAttributeName(final String s)
+  {
+    this.attributeName = s;
+  }
+
+
+  /** {@inheritDoc} */
+  protected Attribute processResult(
+    final SearchCriteria sc,
+    final Attribute attr)
+    throws NamingException
+  {
+    Attribute newAttr = null;
+    if (attr != null) {
+      newAttr = new BasicAttribute(attr.getID(), attr.isOrdered());
+      if (attr.getID().equals(this.attributeName)) {
+        final NamingEnumeration<?> en = attr.getAll();
+        while (en.hasMore()) {
+          final Object rawValue = this.processValue(sc, en.next());
+          if (rawValue instanceof String) {
+            final List<String> recursiveValues = this.recursiveSearch(
+              (String) rawValue,
+              new ArrayList<String>());
+            for (String s : recursiveValues) {
+              newAttr.add(this.processValue(sc, s));
+            }
+          } else {
+            newAttr.add(rawValue);
+          }
+        }
+      } else {
+        final NamingEnumeration<?> en = attr.getAll();
+        while (en.hasMore()) {
+          newAttr.add(this.processValue(sc, en.next()));
+        }
+      }
+    }
+    return newAttr;
+  }
+
+
+  /**
+   * Recursively gets the attribute {@link #attributeName} for the supplied dn.
+   *
+   * @param  dn  to get attribute for
+   * @param  searchedDns  list of DNs that have been searched for
+   *
+   * @return  list of attribute values found by recursively searching
+   *
+   * @throws  NamingException  if a search error occurs
+   */
+  private List<String> recursiveSearch(
+    final String dn,
+    final List<String> searchedDns)
+    throws NamingException
+  {
+    final List<String> results = new ArrayList<String>();
+    if (!searchedDns.contains(dn)) {
+
+      Attributes attrs = null;
+      try {
+        attrs = this.ldap.getAttributes(dn, new String[] {this.attributeName});
+        results.add(dn);
+      } catch (NamingException e) {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn(
+            "Error retreiving attribute: " + this.attributeName,
+            e);
+        }
+      }
+      searchedDns.add(dn);
+      if (attrs != null) {
+        final Attribute attr = attrs.get(this.attributeName);
+        if (attr != null) {
+          final NamingEnumeration<?> en = attr.getAll();
+          while (en.hasMore()) {
+            final Object rawValue = en.next();
+            if (rawValue instanceof String) {
+              results.addAll(
+                this.recursiveSearch((String) rawValue, searchedDns));
+            }
+          }
+        }
+      }
+    }
+    return results;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/RecursiveSearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/RecursiveSearchResultHandler.java
new file mode 100644
index 0000000..a0f86ff
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/RecursiveSearchResultHandler.java
@@ -0,0 +1,323 @@
+/*
+  $Id: RecursiveSearchResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.Ldap;
+
+/**
+ * <code>RecursiveSearchResultHandler</code> recursively searches based on a
+ * supplied attribute and merges those results into the original result set. For
+ * the following LDIF:
+ *
+ * <pre>
+   dn: uugid=group1,ou=groups,dc=vt,dc=edu
+   uugid: group1
+   member: uugid=group2,ou=groups,dc=vt,dc=edu
+
+   dn: uugid=group2,ou=groups,dc=vt,dc=edu
+   uugid: group2
+ * </pre>
+ *
+ * <p>With the following code:</p>
+ *
+ * <pre>
+   RecursiveSearchResultHandler rsh = new RecurseSearchResultHandler(
+     ldap, "member", new String[]{"uugid"});
+ * </pre>
+ *
+ * <p>Will produce this result for the query (uugid=group1):</p>
+ *
+ * <pre>
+   dn: uugid=group1,ou=groups,dc=vt,dc=edu
+   uugid: group1
+   uugid: group2
+   member: uugid=group2,ou=groups,dc=vt,dc=edu
+ * </pre>
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class RecursiveSearchResultHandler extends CopySearchResultHandler
+  implements ExtendedSearchResultHandler
+{
+
+  /** Ldap to use for searching. */
+  private Ldap ldap;
+
+  /** Attribute to recursively search on. */
+  private String searchAttribute;
+
+  /** Attribute(s) to merge. */
+  private String[] mergeAttributes;
+
+  /** Attributes to return when searching, mergeAttributes + searchAttribute. */
+  private String[] retAttrs;
+
+
+  /** Default constructor. */
+  public RecursiveSearchResultHandler() {}
+
+
+  /**
+   * Creates a new <code>RecursiveAttributeHandler</code> with the supplied
+   * search attribute and merge attributes.
+   *
+   * @param  searchAttr  <code>String</code>
+   * @param  mergeAttrs  <code>String[]</code>
+   */
+  public RecursiveSearchResultHandler(
+    final String searchAttr,
+    final String[] mergeAttrs)
+  {
+    this(null, searchAttr, mergeAttrs);
+  }
+
+
+  /**
+   * Creates a new <code>RecursiveAttributeHandler</code> with the supplied
+   * ldap, search attribute, and merge attributes.
+   *
+   * @param  l  <code>Ldap</code>
+   * @param  searchAttr  <code>String</code>
+   * @param  mergeAttrs  <code>String[]</code>
+   */
+  public RecursiveSearchResultHandler(
+    final Ldap l,
+    final String searchAttr,
+    final String[] mergeAttrs)
+  {
+    this.ldap = l;
+    this.searchAttribute = searchAttr;
+    this.mergeAttributes = mergeAttrs;
+    this.initalizeReturnAttributes();
+  }
+
+
+  /** {@inheritDoc} */
+  public Ldap getSearchResultLdap()
+  {
+    return this.ldap;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setSearchResultLdap(final Ldap l)
+  {
+    this.ldap = l;
+  }
+
+
+  /**
+   * Returns the attribute name that will be recursively searched on.
+   *
+   * @return  <code>String</code> attribute name
+   */
+  public String getSearchAttribute()
+  {
+    return this.searchAttribute;
+  }
+
+
+  /**
+   * Sets the attribute name that will be recursively searched on.
+   *
+   * @param  s  <code>String</code>
+   */
+  public void setSearchAttribute(final String s)
+  {
+    this.searchAttribute = s;
+    this.initalizeReturnAttributes();
+  }
+
+
+  /**
+   * Returns the attribute names that will be merged by the recursive search.
+   *
+   * @return  <code>String[]</code> attribute names
+   */
+  public String[] getMergeAttributes()
+  {
+    return this.mergeAttributes;
+  }
+
+
+  /**
+   * Sets the attribute name that will be merged by the recursive search.
+   *
+   * @param  s  <code>String[]</code>
+   */
+  public void setMergeAttributes(final String[] s)
+  {
+    this.mergeAttributes = s;
+    this.initalizeReturnAttributes();
+  }
+
+
+  /**
+   * Initializes the return attributes array. Must be called after both
+   * searchAttribute and mergeAttributes have been set.
+   */
+  protected void initalizeReturnAttributes()
+  {
+    if (this.mergeAttributes != null && this.searchAttribute != null) {
+      // return attributes must include the search attribute
+      this.retAttrs = new String[this.mergeAttributes.length + 1];
+      System.arraycopy(
+        this.mergeAttributes,
+        0,
+        this.retAttrs,
+        0,
+        this.mergeAttributes.length);
+      this.retAttrs[this.retAttrs.length - 1] = this.searchAttribute;
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public List<SearchResult> process(
+    final SearchCriteria sc,
+    final NamingEnumeration<? extends SearchResult> en,
+    final Class<?>[] ignore)
+    throws NamingException
+  {
+    return this.processInternal(super.process(sc, en, ignore));
+  }
+
+
+  /** {@inheritDoc} */
+  public List<SearchResult> process(
+    final SearchCriteria sc,
+    final List<? extends SearchResult> l)
+    throws NamingException
+  {
+    return this.processInternal(super.process(sc, l));
+  }
+
+
+  /**
+   * Recursively searches a list of attributes and merges those results with the
+   * existing search result set.
+   *
+   * @param  results  <code>List</code> of search results to merge with
+   *
+   * @return  <code>List</code> of merged search results
+   *
+   * @throws  NamingException  if an error occurs reading attribute values
+   */
+  private List<SearchResult> processInternal(final List<SearchResult> results)
+    throws NamingException
+  {
+    for (SearchResult sr : results) {
+      final List<String> searchedDns = new ArrayList<String>();
+      if (sr.getAttributes().get(this.searchAttribute) != null) {
+        searchedDns.add(sr.getName());
+        this.readSearchAttribute(sr.getAttributes(), searchedDns);
+      } else {
+        this.recursiveSearch(sr.getName(), sr.getAttributes(), searchedDns);
+      }
+    }
+    return results;
+  }
+
+
+  /**
+   * Reads the values of {@link #searchAttribute} from the supplied attributes
+   * and calls {@link #recursiveSearch} for each.
+   *
+   * @param  attrs  to read
+   * @param  searchedDns  list of DNs whose attributes have been read
+   *
+   * @throws  NamingException  if a search error occurs
+   */
+  private void readSearchAttribute(
+    final Attributes attrs,
+    final List<String> searchedDns)
+    throws NamingException
+  {
+    if (attrs != null) {
+      final Attribute attr = attrs.get(this.searchAttribute);
+      if (attr != null) {
+        final NamingEnumeration<?> en = attr.getAll();
+        while (en.hasMore()) {
+          final Object rawValue = en.next();
+          if (rawValue instanceof String) {
+            this.recursiveSearch((String) rawValue, attrs, searchedDns);
+          }
+        }
+      }
+    }
+  }
+
+
+  /**
+   * Recursively gets the attribute(s) {@link #mergeAttributes} for the supplied
+   * dn and adds the values to the supplied attributes.
+   *
+   * @param  dn  to get attribute(s) for
+   * @param  attrs  to merge with
+   * @param  searchedDns  list of DNs that have been searched for
+   *
+   * @throws  NamingException  if a search error occurs
+   */
+  private void recursiveSearch(
+    final String dn,
+    final Attributes attrs,
+    final List<String> searchedDns)
+    throws NamingException
+  {
+    if (!searchedDns.contains(dn)) {
+
+      Attributes newAttrs = null;
+      try {
+        newAttrs = this.ldap.getAttributes(dn, this.retAttrs);
+      } catch (NamingException e) {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn(
+            "Error retreiving attribute(s): " + Arrays.toString(this.retAttrs),
+            e);
+        }
+      }
+      searchedDns.add(dn);
+
+      if (newAttrs != null) {
+        // recursively search new attributes
+        this.readSearchAttribute(newAttrs, searchedDns);
+
+        // merge new attribute values
+        for (String s : this.mergeAttributes) {
+          final Attribute newAttr = newAttrs.get(s);
+          if (newAttr != null) {
+            final Attribute oldAttr = attrs.get(s);
+            if (oldAttr == null) {
+              attrs.put(newAttr);
+            } else {
+              final NamingEnumeration<?> newValues = newAttr.getAll();
+              while (newValues.hasMore()) {
+                oldAttr.add(newValues.next());
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/ResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/ResultHandler.java
new file mode 100644
index 0000000..01e5622
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/ResultHandler.java
@@ -0,0 +1,77 @@
+/*
+  $Id: ResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.util.List;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+
+/**
+ * ResultHandler provides post search processing of ldap results.
+ *
+ * @param  <R>  type of result
+ * @param  <O>  type of output
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public interface ResultHandler<R, O>
+{
+
+
+  /**
+   * Process the results from an ldap search.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to perform the search
+   * @param  en  <code>NamingEnumeration</code> of search results
+   *
+   * @return  <code>List</code> of result objects
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  List<O> process(SearchCriteria sc, NamingEnumeration<? extends R> en)
+    throws NamingException;
+
+
+  /**
+   * Process the results from an ldap search.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to perform the search
+   * @param  en  <code>NamingEnumeration</code> of search results
+   * @param  ignore  <code>Class[]</code> of exception types to ignore results
+   *
+   * @return  <code>List</code> of result objects
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  List<O> process(
+    SearchCriteria sc,
+    NamingEnumeration<? extends R> en,
+    Class<?>[] ignore)
+    throws NamingException;
+
+
+  /**
+   * Process the results from an ldap search.
+   *
+   * @param  sc  <code>SearchCriteria</code> used to perform the search
+   * @param  l  <code>List</code> of search results
+   *
+   * @return  <code>List</code> of result objects
+   *
+   * @throws  NamingException  if the LDAP returns an error
+   */
+  List<O> process(SearchCriteria sc, List<? extends R> l)
+    throws NamingException;
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/SearchCriteria.java b/src/main/java/edu/vt/middleware/ldap/handler/SearchCriteria.java
new file mode 100644
index 0000000..4f43bde
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/SearchCriteria.java
@@ -0,0 +1,186 @@
+/*
+  $Id: SearchCriteria.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.directory.Attributes;
+
+/**
+ * <code>SearchCriteria</code> contains the attributes used to perform ldap
+ * searches.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class SearchCriteria
+{
+
+  /** dn. */
+  private String dn;
+
+  /** filter. */
+  private String filter;
+
+  /** filter arguments. */
+  private Object[] filterArgs;
+
+  /** return attributes. */
+  private String[] returnAttrs;
+
+  /** match attributes. */
+  private Attributes matchAttrs;
+
+
+  /** Default constructor. */
+  public SearchCriteria() {}
+
+
+  /**
+   * Creates a new search criteria with the supplied dn.
+   *
+   * @param  s  to set dn
+   */
+  public SearchCriteria(final String s)
+  {
+    this.dn = s;
+  }
+
+
+  /**
+   * Gets the dn.
+   *
+   * @return  dn
+   */
+  public String getDn()
+  {
+    return this.dn;
+  }
+
+
+  /**
+   * Sets the dn.
+   *
+   * @param  s  to set dn
+   */
+  public void setDn(final String s)
+  {
+    this.dn = s;
+  }
+
+
+  /**
+   * Gets the filter.
+   *
+   * @return  filter
+   */
+  public String getFilter()
+  {
+    return this.filter;
+  }
+
+
+  /**
+   * Sets the filter.
+   *
+   * @param  s  to set filter
+   */
+  public void setFilter(final String s)
+  {
+    this.filter = s;
+  }
+
+
+  /**
+   * Gets the filter arguments.
+   *
+   * @return  filter args
+   */
+  public Object[] getFilterArgs()
+  {
+    return this.filterArgs;
+  }
+
+
+  /**
+   * Sets the filter arguments.
+   *
+   * @param  o  to set filter argumets
+   */
+  public void setFilterArgs(final Object[] o)
+  {
+    this.filterArgs = o;
+  }
+
+
+  /**
+   * Gets the return attributes.
+   *
+   * @return  return attributes
+   */
+  public String[] getReturnAttrs()
+  {
+    return this.returnAttrs;
+  }
+
+
+  /**
+   * Sets the return attributes.
+   *
+   * @param  s  to set return attributes
+   */
+  public void setReturnAttrs(final String[] s)
+  {
+    this.returnAttrs = s;
+  }
+
+
+  /**
+   * Gets the match attributes.
+   *
+   * @return  match attributes
+   */
+  public Attributes getMatchAttrs()
+  {
+    return this.matchAttrs;
+  }
+
+
+  /**
+   * Sets the match attributes.
+   *
+   * @param  a  to set match attributes
+   */
+  public void setMatchAttrs(final Attributes a)
+  {
+    this.matchAttrs = a;
+  }
+
+
+  /**
+   * This returns a string representation of this search criteria.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return
+      String.format(
+        "dn=%s,filter=%s,filterArgs=%s,returnAttrs=%s,matchAttrs=%s",
+        this.dn,
+        this.filter,
+        this.filterArgs,
+        this.returnAttrs,
+        this.matchAttrs);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/SearchResultHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/SearchResultHandler.java
new file mode 100644
index 0000000..4f82a37
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/SearchResultHandler.java
@@ -0,0 +1,43 @@
+/*
+  $Id: SearchResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import javax.naming.directory.SearchResult;
+
+/**
+ * SearchResultHandler provides post search processing of ldap search results.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public interface SearchResultHandler
+  extends ResultHandler<SearchResult, SearchResult>
+{
+
+
+  /**
+   * Gets the attribute handlers.
+   *
+   * @return  <code>AttributeHandler[]</code>
+   */
+  AttributeHandler[] getAttributeHandler();
+
+
+  /**
+   * Sets the attribute handlers.
+   *
+   * @param  ah  <code>AttributeHandler[]</code>
+   */
+  void setAttributeHandler(final AttributeHandler[] ah);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/handler/TlsConnectionHandler.java b/src/main/java/edu/vt/middleware/ldap/handler/TlsConnectionHandler.java
new file mode 100644
index 0000000..f18fee4
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/handler/TlsConnectionHandler.java
@@ -0,0 +1,266 @@
+/*
+  $Id: TlsConnectionHandler.java 1616 2010-09-21 17:22:27Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1616 $
+  Updated: $Date: 2010-09-21 18:22:27 +0100 (Tue, 21 Sep 2010) $
+*/
+package edu.vt.middleware.ldap.handler;
+
+import java.io.IOException;
+import java.util.Hashtable;
+import javax.naming.CommunicationException;
+import javax.naming.NamingException;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.StartTlsRequest;
+import javax.naming.ldap.StartTlsResponse;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.LdapConstants;
+
+/**
+ * <code>TlsConnectionHandler</code> creates a new <code>LdapContext</code>
+ * using environment properties obtained from {@link
+ * LdapConfig#getEnvironment()} and then invokes the startTLS extended operation
+ * on the context. <code>SSLSocketFactory</code> and <code>
+ * HostnameVerifier</code> properties are used from the <code>
+ * LdapContext</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1616 $
+ */
+public class TlsConnectionHandler extends DefaultConnectionHandler
+{
+
+  /** Start TLS response. */
+  private StartTlsResponse startTlsResponse;
+
+  /**
+   * Whether to call {@link StartTlsResponse#close()} when {@link #close()} is
+   * called.
+   */
+  private boolean stopTlsOnClose;
+
+
+  /** Default constructor. */
+  public TlsConnectionHandler() {}
+
+
+  /**
+   * Creates a new <code>TlsConnectionHandler</code> with the supplied ldap
+   * config.
+   *
+   * @param  lc  ldap config
+   */
+  public TlsConnectionHandler(final LdapConfig lc)
+  {
+    super(lc);
+  }
+
+
+  /**
+   * Copy constructor for <code>TlsConnectionHandler</code>.
+   *
+   * @param  ch  to copy properties from
+   */
+  public TlsConnectionHandler(final TlsConnectionHandler ch)
+  {
+    this.setLdapConfig(ch.getLdapConfig());
+    this.setConnectionStrategy(ch.getConnectionStrategy());
+    this.setConnectionRetryExceptions(ch.getConnectionRetryExceptions());
+    this.setConnectionCount(ch.getConnectionCount());
+    this.setStopTlsOnClose(ch.getStopTlsOnClose());
+  }
+
+
+  /**
+   * Returns whether to call {@link StartTlsResponse#close()} when {@link
+   * #close()} is called.
+   *
+   * @return  stop TLS on close
+   */
+  public boolean getStopTlsOnClose()
+  {
+    return this.stopTlsOnClose;
+  }
+
+
+  /**
+   * Sets whether to call {@link StartTlsResponse#close()} when {@link #close()}
+   * is called.
+   *
+   * @param  b  stop TLS on close
+   */
+  public void setStopTlsOnClose(final boolean b)
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting stopTlsOnClose: " + b);
+    }
+    this.stopTlsOnClose = b;
+  }
+
+
+  /**
+   * This returns the startTLS response created by a call to {@link
+   * #connect(String, Object)}.
+   *
+   * @return  start tls response
+   */
+  public StartTlsResponse getStartTlsResponse()
+  {
+    return this.startTlsResponse;
+  }
+
+
+  /** {@inheritDoc} */
+  protected void connectInternal(
+    final String authtype,
+    final String dn,
+    final Object credential,
+    final Hashtable<String, Object> env)
+    throws NamingException
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Bind with the following parameters:");
+      this.logger.debug("  authtype = " + authtype);
+      this.logger.debug("  dn = " + dn);
+      if (this.config.getLogCredentials()) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("  credential = " + credential);
+        }
+      } else {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("  credential = <suppressed>");
+        }
+      }
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("  env = " + env);
+      }
+    }
+
+    env.put(LdapConstants.VERSION, LdapConstants.VERSION_THREE);
+    try {
+      this.context = new InitialLdapContext(env, null);
+      this.startTlsResponse = this.startTls(this.context);
+      // note that when using simple authentication (the default),
+      // if the credential is null the provider will automatically revert the
+      // authentication to none
+      this.context.addToEnvironment(LdapConstants.AUTHENTICATION, authtype);
+      if (dn != null) {
+        this.context.addToEnvironment(LdapConstants.PRINCIPAL, dn);
+        if (credential != null) {
+          this.context.addToEnvironment(LdapConstants.CREDENTIALS, credential);
+        }
+      }
+      this.context.reconnect(null);
+    } catch (NamingException e) {
+      if (this.context != null) {
+        try {
+          this.context.close();
+        } finally {
+          this.context = null;
+        }
+      }
+      throw e;
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void close()
+    throws NamingException
+  {
+    try {
+      if (this.stopTlsOnClose) {
+        this.stopTls(this.startTlsResponse);
+      }
+    } catch (NamingException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error stopping TLS", e);
+      }
+    } finally {
+      this.startTlsResponse = null;
+      super.close();
+    }
+  }
+
+
+  /**
+   * This will attempt to StartTLS with the supplied <code>LdapContext</code>.
+   *
+   * @param  ctx  <code>LdapContext</code>
+   *
+   * @return  <code>StartTlsResponse</code>
+   *
+   * @throws  NamingException  if an error occurs while requesting an extended
+   * operation
+   */
+  public StartTlsResponse startTls(final LdapContext ctx)
+    throws NamingException
+  {
+    StartTlsResponse tls = null;
+    try {
+      tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
+      if (this.config.useHostnameVerifier()) {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace(
+            "TLS hostnameVerifier = " + this.config.getHostnameVerifier());
+        }
+        tls.setHostnameVerifier(this.config.getHostnameVerifier());
+      }
+      if (this.config.useSslSocketFactory()) {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace(
+            "TLS sslSocketFactory = " + this.config.getSslSocketFactory());
+        }
+        tls.negotiate(this.config.getSslSocketFactory());
+      } else {
+        tls.negotiate();
+      }
+    } catch (IOException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Could not negotiate TLS connection", e);
+      }
+      throw new CommunicationException(e.getMessage());
+    }
+    return tls;
+  }
+
+
+  /**
+   * This will attempt to StopTLS with the supplied <code>
+   * StartTlsResponse</code>.
+   *
+   * @param  tls  <code>StartTlsResponse</code>
+   *
+   * @throws  NamingException  if an error occurs while closing the TLS
+   * connection
+   */
+  public void stopTls(final StartTlsResponse tls)
+    throws NamingException
+  {
+    if (tls != null) {
+      try {
+        tls.close();
+      } catch (IOException e) {
+        if (this.logger.isErrorEnabled()) {
+          this.logger.error("Could not close TLS connection", e);
+        }
+        throw new CommunicationException(e.getMessage());
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public TlsConnectionHandler newInstance()
+  {
+    return new TlsConnectionHandler(this);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/AbstractLoginModule.java b/src/main/java/edu/vt/middleware/ldap/jaas/AbstractLoginModule.java
new file mode 100644
index 0000000..17f9c75
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/AbstractLoginModule.java
@@ -0,0 +1,505 @@
+/*
+  $Id: AbstractLoginModule.java 1878 2011-04-05 15:15:00Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1878 $
+  Updated: $Date: 2011-04-05 16:15:00 +0100 (Tue, 05 Apr 2011) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.auth.Authenticator;
+import edu.vt.middleware.ldap.auth.AuthenticatorConfig;
+import edu.vt.middleware.ldap.bean.LdapAttribute;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.props.LdapProperties;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractLoginModule</code> provides functionality common to ldap based
+ * login modules.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1878 $ $Date: 2011-04-05 16:15:00 +0100 (Tue, 05 Apr 2011) $
+ */
+public abstract class AbstractLoginModule implements LoginModule
+{
+
+  /** Constant for login name stored in shared state. */
+  public static final String LOGIN_NAME = "javax.security.auth.login.name";
+
+  /** Constant for entryDn stored in shared state. */
+  public static final String LOGIN_DN =
+    "edu.vt.middleware.ldap.jaas.login.entryDn";
+
+  /** Constant for login password stored in shared state. */
+  public static final String LOGIN_PASSWORD =
+    "javax.security.auth.login.password";
+
+  /** Regular expression for ldap properties to ignore. */
+  private static final String IGNORE_LDAP_REGEX =
+    "useFirstPass|tryFirstPass|storePass|" +
+    "setLdapPrincipal|setLdapDnPrincipal|setLdapCredential|" +
+    "defaultRole|principalGroupName|roleGroupName|" +
+    "userRoleAttribute|roleFilter|roleAttribute|noResultsIsError";
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Initialized subject. */
+  protected Subject subject;
+
+  /** Initialized callback handler. */
+  protected CallbackHandler callbackHandler;
+
+  /** Shared state from other login module. */
+  @SuppressWarnings("unchecked")
+  protected Map sharedState;
+
+  /** Whether credentials from the shared state should be used. */
+  protected boolean useFirstPass;
+
+  /**
+   * Whether credentials from the shared state should be used if they are
+   * available.
+   */
+  protected boolean tryFirstPass;
+
+  /** Whether credentials should be stored in the shared state map. */
+  protected boolean storePass;
+
+  /** Whether credentials should be removed from the shared state map. */
+  protected boolean clearPass;
+
+  /** Whether ldap principal data should be set. */
+  protected boolean setLdapPrincipal;
+
+  /** Whether ldap dn principal data should be set. */
+  protected boolean setLdapDnPrincipal;
+
+  /** Whether ldap credential data should be set. */
+  protected boolean setLdapCredential;
+
+  /** Default roles. */
+  protected List<LdapRole> defaultRole = new ArrayList<LdapRole>();
+
+  /** Name of group to add all principals to. */
+  protected String principalGroupName;
+
+  /** Name of group to add all roles to. */
+  protected String roleGroupName;
+
+  /** Whether login was successful. */
+  protected boolean loginSuccess;
+
+  /** Whether commit was successful. */
+  protected boolean commitSuccess;
+
+  /** Principals to add to the subject. */
+  protected Set<Principal> principals;
+
+  /** Credentials to add to the subject. */
+  protected Set<LdapCredential> credentials;
+
+  /** Roles to add to the subject. */
+  protected Set<Principal> roles;
+
+
+  /** {@inheritDoc} */
+  public void initialize(
+    final Subject subject,
+    final CallbackHandler callbackHandler,
+    final Map<String, ?> sharedState,
+    final Map<String, ?> options)
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("Begin initialize");
+    }
+    this.subject = subject;
+    this.callbackHandler = callbackHandler;
+    this.sharedState = sharedState;
+
+    final Iterator<String> i = options.keySet().iterator();
+    while (i.hasNext()) {
+      final String key = i.next();
+      final String value = (String) options.get(key);
+      if (key.equalsIgnoreCase("useFirstPass")) {
+        this.useFirstPass = Boolean.valueOf(value);
+      } else if (key.equalsIgnoreCase("tryFirstPass")) {
+        this.tryFirstPass = Boolean.valueOf(value);
+      } else if (key.equalsIgnoreCase("storePass")) {
+        this.storePass = Boolean.valueOf(value);
+      } else if (key.equalsIgnoreCase("clearPass")) {
+        this.clearPass = Boolean.valueOf(value);
+      } else if (key.equalsIgnoreCase("setLdapPrincipal")) {
+        this.setLdapPrincipal = Boolean.valueOf(value);
+      } else if (key.equalsIgnoreCase("setLdapDnPrincipal")) {
+        this.setLdapDnPrincipal = Boolean.valueOf(value);
+      } else if (key.equalsIgnoreCase("setLdapCredential")) {
+        this.setLdapCredential = Boolean.valueOf(value);
+      } else if (key.equalsIgnoreCase("defaultRole")) {
+        for (String s : value.split(",")) {
+          this.defaultRole.add(new LdapRole(s.trim()));
+        }
+      } else if (key.equalsIgnoreCase("principalGroupName")) {
+        this.principalGroupName = value;
+      } else if (key.equalsIgnoreCase("roleGroupName")) {
+        this.roleGroupName = value;
+      }
+    }
+
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("useFirstPass = " + this.useFirstPass);
+      this.logger.debug("tryFirstPass = " + this.tryFirstPass);
+      this.logger.debug("storePass = " + this.storePass);
+      this.logger.debug("clearPass = " + this.clearPass);
+      this.logger.debug("setLdapPrincipal = " + this.setLdapPrincipal);
+      this.logger.debug("setLdapDnPrincipal = " + this.setLdapDnPrincipal);
+      this.logger.debug("setLdapCredential = " + this.setLdapCredential);
+      this.logger.debug("defaultRole = " + this.defaultRole);
+      this.logger.debug("principalGroupName = " + this.principalGroupName);
+      this.logger.debug("roleGroupName = " + this.roleGroupName);
+    }
+
+    this.principals = new TreeSet<Principal>();
+    this.credentials = new HashSet<LdapCredential>();
+    this.roles = new TreeSet<Principal>();
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract boolean login()
+    throws LoginException;
+
+
+  /** {@inheritDoc} */
+  public boolean commit()
+    throws LoginException
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("Begin commit");
+    }
+    if (!this.loginSuccess) {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Login failed");
+      }
+      return false;
+    }
+
+    if (this.subject.isReadOnly()) {
+      this.clearState();
+      throw new LoginException("Subject is read-only.");
+    }
+    this.subject.getPrincipals().addAll(this.principals);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        "Committed the following principals: " + this.principals);
+    }
+    this.subject.getPrivateCredentials().addAll(this.credentials);
+    this.subject.getPrincipals().addAll(this.roles);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Committed the following roles: " + this.roles);
+    }
+    if (this.principalGroupName != null) {
+      final LdapGroup group = new LdapGroup(this.principalGroupName);
+      for (Principal principal : this.principals) {
+        group.addMember(principal);
+      }
+      subject.getPrincipals().add(group);
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug(
+          "Committed the following principal group: " + group);
+      }
+    }
+    if (this.roleGroupName != null) {
+      final LdapGroup group = new LdapGroup(this.roleGroupName);
+      for (Principal role : this.roles) {
+        group.addMember(role);
+      }
+      subject.getPrincipals().add(group);
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Committed the following role group: " + group);
+      }
+    }
+
+    this.clearState();
+    this.commitSuccess = true;
+    return true;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean abort()
+    throws LoginException
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("Begin abort");
+    }
+    if (!this.loginSuccess) {
+      return false;
+    } else if (this.loginSuccess && !this.commitSuccess) {
+      this.loginSuccess = false;
+      this.clearState();
+    } else {
+      this.logout();
+    }
+    return true;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean logout()
+    throws LoginException
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("Begin logout");
+    }
+    if (this.subject.isReadOnly()) {
+      this.clearState();
+      throw new LoginException("Subject is read-only.");
+    }
+
+    final Iterator<LdapPrincipal> prinIter = this.subject.getPrincipals(
+      LdapPrincipal.class).iterator();
+    while (prinIter.hasNext()) {
+      this.subject.getPrincipals().remove(prinIter.next());
+    }
+
+    final Iterator<LdapDnPrincipal> dnPrinIter = this.subject.getPrincipals(
+      LdapDnPrincipal.class).iterator();
+    while (dnPrinIter.hasNext()) {
+      this.subject.getPrincipals().remove(dnPrinIter.next());
+    }
+
+    final Iterator<LdapRole> roleIter = this.subject.getPrincipals(
+      LdapRole.class).iterator();
+    while (roleIter.hasNext()) {
+      this.subject.getPrincipals().remove(roleIter.next());
+    }
+
+    final Iterator<LdapGroup> groupIter = this.subject.getPrincipals(
+      LdapGroup.class).iterator();
+    while (groupIter.hasNext()) {
+      this.subject.getPrincipals().remove(groupIter.next());
+    }
+
+    final Iterator<LdapCredential> credIter = this.subject
+        .getPrivateCredentials(LdapCredential.class).iterator();
+    while (credIter.hasNext()) {
+      this.subject.getPrivateCredentials().remove(credIter.next());
+    }
+
+    this.clearState();
+    this.loginSuccess = false;
+    this.commitSuccess = false;
+    return true;
+  }
+
+
+  /**
+   * This constructs a new <code>Ldap</code> with the supplied jaas options.
+   *
+   * @param  options  <code>Map</code>
+   *
+   * @return  <code>Ldap</code>
+   */
+  public static Ldap createLdap(final Map<String, ?> options)
+  {
+    final LdapConfig ldapConfig = new LdapConfig();
+    final LdapProperties ldapProperties = new LdapProperties(ldapConfig);
+    final Iterator<String> i = options.keySet().iterator();
+    while (i.hasNext()) {
+      final String key = i.next();
+      final String value = (String) options.get(key);
+      if (!key.matches(IGNORE_LDAP_REGEX)) {
+        ldapProperties.setProperty(key, value);
+      }
+    }
+    ldapProperties.configure();
+    return new Ldap(ldapConfig);
+  }
+
+
+  /**
+   * This constructs a new <code>Authenticator</code> with the supplied jaas
+   * options.
+   *
+   * @param  options  <code>Map</code>
+   *
+   * @return  <code>Authenticator</code>
+   */
+  public static Authenticator createAuthenticator(final Map<String, ?> options)
+  {
+    final AuthenticatorConfig authConfig = new AuthenticatorConfig();
+    final LdapProperties authProperties = new LdapProperties(authConfig);
+    final Iterator<String> i = options.keySet().iterator();
+    while (i.hasNext()) {
+      final String key = i.next();
+      final String value = (String) options.get(key);
+      if (!key.matches(IGNORE_LDAP_REGEX)) {
+        authProperties.setProperty(key, value);
+      }
+    }
+    authProperties.configure();
+    return new JaasAuthenticator(authConfig);
+  }
+
+
+  /**
+   * Removes any stateful principals, credentials, or roles stored by login.
+   * Also removes shared state name, dn, and password if clearPass is set.
+   */
+  protected void clearState()
+  {
+    this.principals.clear();
+    this.credentials.clear();
+    this.roles.clear();
+    if (this.clearPass) {
+      this.sharedState.remove(LOGIN_NAME);
+      this.sharedState.remove(LOGIN_PASSWORD);
+      this.sharedState.remove(LOGIN_DN);
+    }
+  }
+
+
+  /**
+   * This attempts to retrieve credentials for the supplied name and password
+   * callbacks. If useFirstPass or tryFirstPass is set, then name and password
+   * data is retrieved from shared state. Otherwise a callback handler is used
+   * to get the data. Set useCallback to force a callback handler to be used.
+   *
+   * @param  nameCb  to set name for
+   * @param  passCb  to set password for
+   * @param  useCallback  whether to force a callback handler
+   *
+   * @throws  LoginException  if the callback handler fails
+   */
+  protected void getCredentials(
+    final NameCallback nameCb,
+    final PasswordCallback passCb,
+    final boolean useCallback)
+    throws LoginException
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("Begin getCredentials");
+      this.logger.trace("  useFistPass = " + this.useFirstPass);
+      this.logger.trace("  tryFistPass = " + this.tryFirstPass);
+      this.logger.trace("  useCallback = " + useCallback);
+      this.logger.trace(
+        "  callbackhandler class = " +
+        this.callbackHandler.getClass().getName());
+      this.logger.trace(
+        "  name callback class = " + nameCb.getClass().getName());
+      this.logger.trace(
+        "  password callback class = " + passCb.getClass().getName());
+    }
+    try {
+      if ((this.useFirstPass || this.tryFirstPass) && !useCallback) {
+        nameCb.setName((String) this.sharedState.get(LOGIN_NAME));
+        passCb.setPassword((char[]) this.sharedState.get(LOGIN_PASSWORD));
+      } else if (this.callbackHandler != null) {
+        this.callbackHandler.handle(new Callback[] {nameCb, passCb});
+      } else {
+        throw new LoginException(
+          "No CallbackHandler available. " +
+          "Set useFirstPass, tryFirstPass, or provide a CallbackHandler");
+      }
+    } catch (IOException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error reading data from callback handler", e);
+      }
+      this.loginSuccess = false;
+      throw new LoginException(e.getMessage());
+    } catch (UnsupportedCallbackException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Unsupported callback", e);
+      }
+      this.loginSuccess = false;
+      throw new LoginException(e.getMessage());
+    }
+  }
+
+
+  /**
+   * This will store the supplied name, password, and entry dn in the stored
+   * state map. storePass must be set for this method to have any affect.
+   *
+   * @param  nameCb  to store
+   * @param  passCb  to store
+   * @param  loginDn  to store
+   */
+  @SuppressWarnings("unchecked")
+  protected void storeCredentials(
+    final NameCallback nameCb,
+    final PasswordCallback passCb,
+    final String loginDn)
+  {
+    if (this.storePass) {
+      if (nameCb != null && nameCb.getName() != null) {
+        this.sharedState.put(LOGIN_NAME, nameCb.getName());
+      }
+      if (passCb != null && passCb.getPassword() != null) {
+        this.sharedState.put(LOGIN_PASSWORD, passCb.getPassword());
+      }
+      if (loginDn != null) {
+        this.sharedState.put(LOGIN_DN, loginDn);
+      }
+    }
+  }
+
+
+  /**
+   * This parses the supplied attributes and returns them as a list of <code>
+   * LdapRole</code>s.
+   *
+   * @param  attributes  <code>Attributes</code>
+   *
+   * @return  <code>List</code>
+   *
+   * @throws  NamingException  if the attributes cannot be parsed
+   */
+  protected List<LdapRole> attributesToRoles(final Attributes attributes)
+    throws NamingException
+  {
+    final List<LdapRole> roles = new ArrayList<LdapRole>();
+    if (attributes != null) {
+      final LdapAttributes ldapAttrs = LdapBeanProvider.getLdapBeanFactory()
+          .newLdapAttributes();
+      ldapAttrs.addAttributes(attributes);
+      for (LdapAttribute ldapAttr : ldapAttrs.getAttributes()) {
+        for (String attrValue : ldapAttr.getStringValues()) {
+          roles.add(new LdapRole(attrValue));
+        }
+      }
+    }
+    return roles;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/JaasAuthenticator.java b/src/main/java/edu/vt/middleware/ldap/jaas/JaasAuthenticator.java
new file mode 100644
index 0000000..b27072f
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/JaasAuthenticator.java
@@ -0,0 +1,93 @@
+/*
+  $Id: JaasAuthenticator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import edu.vt.middleware.ldap.auth.Authenticator;
+import edu.vt.middleware.ldap.auth.AuthenticatorConfig;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationResultHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthorizationHandler;
+
+/**
+ * <code>JaasAuthenticator</code> is the default implementation for JAAS
+ * authentication.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class JaasAuthenticator extends Authenticator
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -7884185473690369247L;
+
+
+  /** Default constructor. */
+  public JaasAuthenticator() {}
+
+
+  /**
+   * This will create a new <code>JaasAuthenticator</code> with the supplied
+   * <code>AuthenticatorConfig</code>.
+   *
+   * @param  authConfig  <code>AuthenticatorConfig</code>
+   */
+  public JaasAuthenticator(final AuthenticatorConfig authConfig)
+  {
+    this.setAuthenticatorConfig(authConfig);
+  }
+
+
+  /** {@inheritDoc} */
+  public Attributes authenticate(
+    final String user,
+    final Object credential,
+    final String[] retAttrs)
+    throws NamingException
+  {
+    return super.authenticate(user, credential, retAttrs);
+  }
+
+
+  /** {@inheritDoc} */
+  public Attributes authenticate(
+    final String user,
+    final Object credential,
+    final String[] retAttrs,
+    final AuthenticationResultHandler[] authHandler,
+    final AuthorizationHandler[] authzHandler)
+    throws NamingException
+  {
+    if (retAttrs != null && retAttrs.length == 0) {
+      return
+        this.authenticateAndAuthorize(
+          this.getDn(user),
+          credential,
+          false,
+          retAttrs,
+          authHandler,
+          authzHandler);
+    } else {
+      return
+        this.authenticateAndAuthorize(
+          this.getDn(user),
+          credential,
+          true,
+          retAttrs,
+          authHandler,
+          authzHandler);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/LdapCredential.java b/src/main/java/edu/vt/middleware/ldap/jaas/LdapCredential.java
new file mode 100644
index 0000000..e5181ad
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/LdapCredential.java
@@ -0,0 +1,93 @@
+/*
+  $Id: LdapCredential.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.io.Serializable;
+
+/**
+ * <code>LdapCredential</code> provides a custom implementation for adding LDAP
+ * credentials to a <code>Subject</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdapCredential implements Serializable
+{
+
+  /** hash code seed. */
+  protected static final int HASH_CODE_SEED = 89;
+
+  /** serial version uid. */
+  private static final long serialVersionUID = 6571981350905290712L;
+
+  /** LDAP credential. */
+  private Object credential;
+
+
+  /**
+   * This creates a new <code>LdapCredential</code> with the supplied
+   * credential.
+   *
+   * @param  credential  <code>Object</code>
+   */
+  public LdapCredential(final Object credential)
+  {
+    this.credential = credential;
+  }
+
+
+  /**
+   * This returns the credential for this <code>LdapCredential</code>.
+   *
+   * @return  <code>Object</code>
+   */
+  public Object getCredential()
+  {
+    return this.credential;
+  }
+
+
+  /**
+   * This returns the supplied Object is equal to this <code>
+   * LdapCredential</code>.
+   *
+   * @param  o  <code>Object</code>
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean equals(final Object o)
+  {
+    if (o == null) {
+      return false;
+    }
+    return
+      o == this ||
+        (this.getClass() == o.getClass() && o.hashCode() == this.hashCode());
+  }
+
+
+  /**
+   * This returns the hash code for this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>int</code>
+   */
+  public int hashCode()
+  {
+    int hc = HASH_CODE_SEED;
+    if (this.credential != null) {
+      hc += this.credential.hashCode();
+    }
+    return hc;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/LdapDnAuthorizationModule.java b/src/main/java/edu/vt/middleware/ldap/jaas/LdapDnAuthorizationModule.java
new file mode 100644
index 0000000..2815625
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/LdapDnAuthorizationModule.java
@@ -0,0 +1,157 @@
+/*
+  $Id: LdapDnAuthorizationModule.java 1878 2011-04-05 15:15:00Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1878 $
+  Updated: $Date: 2011-04-05 16:15:00 +0100 (Tue, 05 Apr 2011) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.security.Principal;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import javax.naming.NamingException;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import com.sun.security.auth.callback.TextCallbackHandler;
+import edu.vt.middleware.ldap.auth.Authenticator;
+
+/**
+ * <code>LdapDnAuthorizationModule</code> provides a JAAS authentication hook
+ * into LDAP DNs. No authentication is performed in this module. The LDAP entry
+ * dn can be stored and shared with other modules.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1878 $ $Date: 2011-04-05 16:15:00 +0100 (Tue, 05 Apr 2011) $
+ */
+public class LdapDnAuthorizationModule extends AbstractLoginModule
+  implements LoginModule
+{
+
+  /** Whether failing to find a DN should raise an exception. */
+  private boolean noResultsIsError;
+
+  /** Authenticator to use against the LDAP. */
+  private Authenticator auth;
+
+
+  /** {@inheritDoc} */
+  public void initialize(
+    final Subject subject,
+    final CallbackHandler callbackHandler,
+    final Map<String, ?> sharedState,
+    final Map<String, ?> options)
+  {
+    super.initialize(subject, callbackHandler, sharedState, options);
+
+    final Iterator<String> i = options.keySet().iterator();
+    while (i.hasNext()) {
+      final String key = i.next();
+      final String value = (String) options.get(key);
+      if (key.equalsIgnoreCase("noResultsIsError")) {
+        this.noResultsIsError = Boolean.valueOf(value);
+      }
+    }
+
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("noResultsIsError = " + this.noResultsIsError);
+    }
+
+    this.auth = createAuthenticator(options);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        "Created authenticator: " + this.auth.getAuthenticatorConfig());
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean login()
+    throws LoginException
+  {
+    try {
+      final NameCallback nameCb = new NameCallback("Enter user: ");
+      final PasswordCallback passCb = new PasswordCallback(
+        "Enter user password: ",
+        false);
+      this.getCredentials(nameCb, passCb, false);
+
+      if (nameCb.getName() == null && this.tryFirstPass) {
+        this.getCredentials(nameCb, passCb, true);
+      }
+
+      final String loginName = nameCb.getName();
+      if (loginName != null && this.setLdapPrincipal) {
+        this.principals.add(new LdapPrincipal(loginName));
+        this.loginSuccess = true;
+      }
+
+      final String loginDn = this.auth.getDn(nameCb.getName());
+      if (loginDn == null && this.noResultsIsError) {
+        this.loginSuccess = false;
+        throw new LoginException("Could not find DN for " + nameCb.getName());
+      }
+      if (loginDn != null && this.setLdapDnPrincipal) {
+        this.principals.add(new LdapDnPrincipal(loginDn));
+        this.loginSuccess = true;
+      }
+      if (this.defaultRole != null && !this.defaultRole.isEmpty()) {
+        this.roles.addAll(this.defaultRole);
+        this.loginSuccess = true;
+      }
+      this.storeCredentials(nameCb, passCb, loginDn);
+    } catch (NamingException e) {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Error occured attempting DN lookup", e);
+      }
+      this.loginSuccess = false;
+      throw new LoginException(e.getMessage());
+    } finally {
+      this.auth.close();
+    }
+    return true;
+  }
+
+
+  /**
+   * This provides command line access to a <code>LdapLoginModule</code>.
+   *
+   * @param  args  <code>String[]</code>
+   *
+   * @throws  Exception  if an error occurs
+   */
+  public static void main(final String[] args)
+    throws Exception
+  {
+    String name = "vt-ldap-dn";
+    if (args.length > 0) {
+      name = args[0];
+    }
+
+    final LoginContext lc = new LoginContext(name, new TextCallbackHandler());
+    lc.login();
+    System.out.println("Authorization succeeded");
+
+    final Set<Principal> principals = lc.getSubject().getPrincipals();
+    System.out.println("Subject Principal(s): ");
+
+    final Iterator<Principal> i = principals.iterator();
+    while (i.hasNext()) {
+      final Principal p = i.next();
+      System.out.println("  " + p.getName());
+    }
+    lc.logout();
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/LdapDnPrincipal.java b/src/main/java/edu/vt/middleware/ldap/jaas/LdapDnPrincipal.java
new file mode 100644
index 0000000..d4e0168
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/LdapDnPrincipal.java
@@ -0,0 +1,138 @@
+/*
+  $Id: LdapDnPrincipal.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.io.Serializable;
+import java.security.Principal;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+
+/**
+ * <code>LdapPrincipal</code> provides a custom implementation for adding LDAP
+ * principals to a <code>Subject</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdapDnPrincipal
+  implements Principal, Serializable, Comparable<Principal>
+{
+
+  /** hash code seed. */
+  protected static final int HASH_CODE_SEED = 80;
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -4530972236127507368L;
+
+  /** LDAP user name. */
+  private String name;
+
+  /** User attributes. */
+  private LdapAttributes attributes = LdapBeanProvider.getLdapBeanFactory()
+      .newLdapAttributes();
+
+
+  /**
+   * This creates a new <code>LdapPrincipal</code> with the supplied name.
+   *
+   * @param  name  <code>String</code>
+   */
+  public LdapDnPrincipal(final String name)
+  {
+    this.name = name;
+  }
+
+
+  /**
+   * This returns the name for this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>String</code>
+   */
+  public String getName()
+  {
+    return this.name;
+  }
+
+
+  /**
+   * This returns the ldap attributes for this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>LdapAttributes</code>
+   */
+  public LdapAttributes getLdapAttributes()
+  {
+    return this.attributes;
+  }
+
+
+  /**
+   * This returns the supplied Object is equal to this <code>
+   * LdapPrincipal</code>.
+   *
+   * @param  o  <code>Object</code>
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean equals(final Object o)
+  {
+    if (o == null) {
+      return false;
+    }
+    return
+      o == this ||
+        (this.getClass() == o.getClass() && o.hashCode() == this.hashCode());
+  }
+
+
+  /**
+   * This returns the hash code for this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>int</code>
+   */
+  public int hashCode()
+  {
+    int hc = HASH_CODE_SEED;
+    if (this.name != null) {
+      hc += this.name.hashCode();
+    }
+    return hc;
+  }
+
+
+  /**
+   * This returns a String representation of this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return String.format("%s%s", this.name, this.attributes);
+  }
+
+
+  /**
+   * This compares the supplied object for order. <code>LdapPrincipal</code> is
+   * always less than any other object. Otherwise principals are compared
+   * lexicographically on name.
+   *
+   * @param  p  <code>Principal</code>
+   *
+   * @return  <code>int</code>
+   */
+  public int compareTo(final Principal p)
+  {
+    return this.name.compareTo(p.getName());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/LdapGroup.java b/src/main/java/edu/vt/middleware/ldap/jaas/LdapGroup.java
new file mode 100644
index 0000000..d308e49
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/LdapGroup.java
@@ -0,0 +1,120 @@
+/*
+  $Id: LdapGroup.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.io.Serializable;
+import java.security.Principal;
+import java.security.acl.Group;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * <code>LdapGroup</code> provides a custom implementation for grouping
+ * principals.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdapGroup implements Group, Serializable
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -342760961669842632L;
+
+  /** LDAP role name. */
+  private String name;
+
+  /** Principal members. */
+  private Set<Principal> members = new HashSet<Principal>();
+
+
+  /**
+   * This creates a new <code>LdapGroup</code> with the supplied name.
+   *
+   * @param  name  <code>String</code>
+   */
+  public LdapGroup(final String name)
+  {
+    this.name = name;
+  }
+
+
+  /**
+   * This returns the name for this <code>LdapGroup</code>.
+   *
+   * @return  <code>String</code>
+   */
+  public String getName()
+  {
+    return this.name;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean addMember(final Principal user)
+  {
+    return this.members.add(user);
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean removeMember(final Principal user)
+  {
+    return this.members.remove(user);
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean isMember(final Principal member)
+  {
+    for (Principal p : this.members) {
+      if (p.getName() != null && p.getName().equals(member.getName())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+
+  /** {@inheritDoc} */
+  public Enumeration<? extends Principal> members()
+  {
+    return Collections.enumeration(this.members);
+  }
+
+
+  /**
+   * Returns an unmodifiable set of the members in this group.
+   *
+   * @return  <code>Set</code> of member principals
+   */
+  public Set<Principal> getMembers()
+  {
+    return Collections.unmodifiableSet(this.members);
+  }
+
+
+  /**
+   * This returns a String representation of this <code>LdapGroup</code>.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return String.format("%s%s", this.name, this.members);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/LdapLoginModule.java b/src/main/java/edu/vt/middleware/ldap/jaas/LdapLoginModule.java
new file mode 100644
index 0000000..9762eb6
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/LdapLoginModule.java
@@ -0,0 +1,205 @@
+/*
+  $Id: LdapLoginModule.java 1878 2011-04-05 15:15:00Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1878 $
+  Updated: $Date: 2011-04-05 16:15:00 +0100 (Tue, 05 Apr 2011) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import javax.naming.AuthenticationException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import com.sun.security.auth.callback.TextCallbackHandler;
+import edu.vt.middleware.ldap.auth.Authenticator;
+
+/**
+ * <code>LdapLoginModule</code> provides a JAAS authentication hook into LDAP
+ * authentication.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1878 $ $Date: 2011-04-05 16:15:00 +0100 (Tue, 05 Apr 2011) $
+ */
+public class LdapLoginModule extends AbstractLoginModule implements LoginModule
+{
+
+  /** User attribute to add to role data. */
+  private String[] userRoleAttribute = new String[0];
+
+  /** Authenticator to use against the LDAP. */
+  private Authenticator auth;
+
+
+  /** {@inheritDoc} */
+  public void initialize(
+    final Subject subject,
+    final CallbackHandler callbackHandler,
+    final Map<String, ?> sharedState,
+    final Map<String, ?> options)
+  {
+    this.setLdapPrincipal = true;
+    this.setLdapCredential = true;
+
+    super.initialize(subject, callbackHandler, sharedState, options);
+
+    final Iterator<String> i = options.keySet().iterator();
+    while (i.hasNext()) {
+      final String key = i.next();
+      final String value = (String) options.get(key);
+      if (key.equalsIgnoreCase("userRoleAttribute")) {
+        if ("*".equals(value)) {
+          this.userRoleAttribute = null;
+        } else {
+          this.userRoleAttribute = value.split(",");
+        }
+      }
+    }
+
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        "userRoleAttribute = " + Arrays.toString(this.userRoleAttribute));
+    }
+
+    this.auth = createAuthenticator(options);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        "Created authenticator: " + this.auth.getAuthenticatorConfig());
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean login()
+    throws LoginException
+  {
+    try {
+      final NameCallback nameCb = new NameCallback("Enter user: ");
+      final PasswordCallback passCb = new PasswordCallback(
+        "Enter user password: ",
+        false);
+      this.getCredentials(nameCb, passCb, false);
+
+      AuthenticationException authEx = null;
+      Attributes attrs = null;
+      try {
+        attrs = this.auth.authenticate(
+          nameCb.getName(),
+          passCb.getPassword(),
+          this.userRoleAttribute);
+        this.roles.addAll(this.attributesToRoles(attrs));
+        if (this.defaultRole != null && !this.defaultRole.isEmpty()) {
+          this.roles.addAll(this.defaultRole);
+        }
+        this.loginSuccess = true;
+      } catch (AuthenticationException e) {
+        if (this.tryFirstPass) {
+          this.getCredentials(nameCb, passCb, true);
+          try {
+            attrs = this.auth.authenticate(
+              nameCb.getName(),
+              passCb.getPassword(),
+              this.userRoleAttribute);
+            this.roles.addAll(this.attributesToRoles(attrs));
+            if (this.defaultRole != null && !this.defaultRole.isEmpty()) {
+              this.roles.addAll(this.defaultRole);
+            }
+            this.loginSuccess = true;
+          } catch (AuthenticationException e2) {
+            authEx = e;
+            this.loginSuccess = false;
+          }
+        } else {
+          authEx = e;
+          this.loginSuccess = false;
+        }
+      }
+      if (!this.loginSuccess) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Authentication failed", authEx);
+        }
+        throw new LoginException(
+          authEx != null ? authEx.getMessage() : "Authentication failed");
+      } else {
+        if (this.setLdapPrincipal) {
+          final LdapPrincipal lp = new LdapPrincipal(nameCb.getName());
+          if (attrs != null) {
+            lp.getLdapAttributes().addAttributes(attrs);
+          }
+          this.principals.add(lp);
+        }
+
+        final String loginDn = this.auth.getDn(nameCb.getName());
+        if (loginDn != null && this.setLdapDnPrincipal) {
+          final LdapDnPrincipal lp = new LdapDnPrincipal(loginDn);
+          if (attrs != null) {
+            lp.getLdapAttributes().addAttributes(attrs);
+          }
+          this.principals.add(lp);
+        }
+        if (this.setLdapCredential) {
+          this.credentials.add(new LdapCredential(passCb.getPassword()));
+        }
+        this.storeCredentials(nameCb, passCb, loginDn);
+      }
+    } catch (NamingException e) {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Error occured attempting authentication", e);
+      }
+      this.loginSuccess = false;
+      throw new LoginException(
+        e != null ? e.getMessage() : "Authentication Error");
+    } finally {
+      this.auth.close();
+    }
+    return true;
+  }
+
+
+  /**
+   * This provides command line access to a <code>LdapLoginModule</code>.
+   *
+   * @param  args  <code>String[]</code>
+   *
+   * @throws  Exception  if an error occurs
+   */
+  public static void main(final String[] args)
+    throws Exception
+  {
+    String name = "vt-ldap";
+    if (args.length > 0) {
+      name = args[0];
+    }
+
+    final LoginContext lc = new LoginContext(name, new TextCallbackHandler());
+    lc.login();
+    System.out.println("Authentication/Authorization succeeded");
+
+    final Set<Principal> principals = lc.getSubject().getPrincipals();
+    System.out.println("Subject Principal(s): ");
+
+    final Iterator<Principal> i = principals.iterator();
+    while (i.hasNext()) {
+      final Principal p = i.next();
+      System.out.println("  " + p);
+    }
+    lc.logout();
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/LdapPrincipal.java b/src/main/java/edu/vt/middleware/ldap/jaas/LdapPrincipal.java
new file mode 100644
index 0000000..825b2bc
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/LdapPrincipal.java
@@ -0,0 +1,138 @@
+/*
+  $Id: LdapPrincipal.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.io.Serializable;
+import java.security.Principal;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+
+/**
+ * <code>LdapPrincipal</code> provides a custom implementation for adding LDAP
+ * principals to a <code>Subject</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdapPrincipal
+  implements Principal, Serializable, Comparable<Principal>
+{
+
+  /** hash code seed. */
+  protected static final int HASH_CODE_SEED = 79;
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -1043578648596801523L;
+
+  /** LDAP user name. */
+  private String name;
+
+  /** User attributes. */
+  private LdapAttributes attributes = LdapBeanProvider.getLdapBeanFactory()
+      .newLdapAttributes();
+
+
+  /**
+   * This creates a new <code>LdapPrincipal</code> with the supplied name.
+   *
+   * @param  name  <code>String</code>
+   */
+  public LdapPrincipal(final String name)
+  {
+    this.name = name;
+  }
+
+
+  /**
+   * This returns the name for this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>String</code>
+   */
+  public String getName()
+  {
+    return this.name;
+  }
+
+
+  /**
+   * This returns the ldap attributes for this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>LdapAttributes</code>
+   */
+  public LdapAttributes getLdapAttributes()
+  {
+    return this.attributes;
+  }
+
+
+  /**
+   * This returns the supplied Object is equal to this <code>
+   * LdapPrincipal</code>.
+   *
+   * @param  o  <code>Object</code>
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean equals(final Object o)
+  {
+    if (o == null) {
+      return false;
+    }
+    return
+      o == this ||
+        (this.getClass() == o.getClass() && o.hashCode() == this.hashCode());
+  }
+
+
+  /**
+   * This returns the hash code for this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>int</code>
+   */
+  public int hashCode()
+  {
+    int hc = HASH_CODE_SEED;
+    if (this.name != null) {
+      hc += this.name.hashCode();
+    }
+    return hc;
+  }
+
+
+  /**
+   * This returns a String representation of this <code>LdapPrincipal</code>.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return String.format("%s%s", this.name, this.attributes);
+  }
+
+
+  /**
+   * This compares the supplied object for order. <code>LdapPrincipal</code> is
+   * always less than any other object. Otherwise principals are compared
+   * lexicographically on name.
+   *
+   * @param  p  <code>Principal</code>
+   *
+   * @return  <code>int</code>
+   */
+  public int compareTo(final Principal p)
+  {
+    return this.name.compareTo(p.getName());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/LdapRole.java b/src/main/java/edu/vt/middleware/ldap/jaas/LdapRole.java
new file mode 100644
index 0000000..e90f943
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/LdapRole.java
@@ -0,0 +1,119 @@
+/*
+  $Id: LdapRole.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.io.Serializable;
+import java.security.Principal;
+
+/**
+ * <code>LdapRole</code> provides a custom implementation for adding LDAP
+ * principals to a <code>Subject</code> that represent roles.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdapRole implements Principal, Serializable, Comparable<Principal>
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = 1427032827399935399L;
+
+  /** LDAP role name. */
+  private String name;
+
+
+  /**
+   * This creates a new <code>LdapRole</code> with the supplied name.
+   *
+   * @param  name  <code>String</code>
+   */
+  public LdapRole(final String name)
+  {
+    this.name = name;
+  }
+
+
+  /**
+   * This returns the name for this <code>LdapRole</code>.
+   *
+   * @return  <code>String</code>
+   */
+  public String getName()
+  {
+    return this.name;
+  }
+
+
+  /**
+   * This returns the supplied Object is equal to this <code>LdapRole</code>.
+   *
+   * @param  o  <code>Object</code>
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean equals(final Object o)
+  {
+    boolean b = false;
+    if (o != null) {
+      if (this != o) {
+        if (o instanceof LdapRole) {
+          if (((LdapRole) o).getName().equals(this.name)) {
+            b = true;
+          }
+        }
+      } else {
+        b = true;
+      }
+    }
+    return b;
+  }
+
+
+  /**
+   * This returns the hash code for this <code>LdapRole</code>.
+   *
+   * @return  <code>int</code>
+   */
+  public int hashCode()
+  {
+    return this.name.hashCode();
+  }
+
+
+  /**
+   * This returns a String representation of this <code>LdapRole</code>.
+   *
+   * @return  <code>String</code>
+   */
+  @Override
+  public String toString()
+  {
+    return this.name;
+  }
+
+
+  /**
+   * This compares the supplied object for order. <code>LdapRole</code> is
+   * always greater than any other object. Otherwise principals are compared
+   * lexicographically on name.
+   *
+   * @param  p  <code>Principal</code>
+   *
+   * @return  <code>int</code>
+   */
+  public int compareTo(final Principal p)
+  {
+    return this.name.compareTo(p.getName());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/jaas/LdapRoleAuthorizationModule.java b/src/main/java/edu/vt/middleware/ldap/jaas/LdapRoleAuthorizationModule.java
new file mode 100644
index 0000000..c34f5d2
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/jaas/LdapRoleAuthorizationModule.java
@@ -0,0 +1,191 @@
+/*
+  $Id: LdapRoleAuthorizationModule.java 1878 2011-04-05 15:15:00Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1878 $
+  Updated: $Date: 2011-04-05 16:15:00 +0100 (Tue, 05 Apr 2011) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import com.sun.security.auth.callback.TextCallbackHandler;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+
+/**
+ * <code>LdapRoleAuthorizationModule</code> provides a JAAS authentication hook
+ * into LDAP roles. No authentication is performed in this module. Role data is
+ * set for the login name in the shared state or for the name returned by the
+ * CallbackHandler.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1878 $ $Date: 2011-04-05 16:15:00 +0100 (Tue, 05 Apr 2011) $
+ */
+public class LdapRoleAuthorizationModule extends AbstractLoginModule
+  implements LoginModule
+{
+
+  /** Ldap filter for role searches. */
+  private String roleFilter;
+
+  /** Role attribute to add to role data. */
+  private String[] roleAttribute = new String[0];
+
+  /** Whether failing to find any roles should raise an exception. */
+  private boolean noResultsIsError;
+
+  /** Ldap to use for searching roles against the LDAP. */
+  private Ldap ldap;
+
+
+  /** {@inheritDoc} */
+  public void initialize(
+    final Subject subject,
+    final CallbackHandler callbackHandler,
+    final Map<String, ?> sharedState,
+    final Map<String, ?> options)
+  {
+    super.initialize(subject, callbackHandler, sharedState, options);
+
+    final Iterator<String> i = options.keySet().iterator();
+    while (i.hasNext()) {
+      final String key = i.next();
+      final String value = (String) options.get(key);
+      if (key.equalsIgnoreCase("roleFilter")) {
+        this.roleFilter = value;
+      } else if (key.equalsIgnoreCase("roleAttribute")) {
+        if ("*".equals(value)) {
+          this.roleAttribute = null;
+        } else {
+          this.roleAttribute = value.split(",");
+        }
+      } else if (key.equalsIgnoreCase("noResultsIsError")) {
+        this.noResultsIsError = Boolean.valueOf(value);
+      }
+    }
+
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("roleFilter = " + this.roleFilter);
+      this.logger.debug(
+        "roleAttribute = " + Arrays.toString(this.roleAttribute));
+      this.logger.debug("noResultsIsError = " + this.noResultsIsError);
+    }
+
+    this.ldap = createLdap(options);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Created ldap: " + this.ldap.getLdapConfig());
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean login()
+    throws LoginException
+  {
+    try {
+      final NameCallback nameCb = new NameCallback("Enter user: ");
+      final PasswordCallback passCb = new PasswordCallback(
+        "Enter user password: ",
+        false);
+      this.getCredentials(nameCb, passCb, false);
+
+      if (nameCb.getName() == null && this.tryFirstPass) {
+        this.getCredentials(nameCb, passCb, true);
+      }
+
+      final String loginName = nameCb.getName();
+      if (loginName != null && this.setLdapPrincipal) {
+        this.principals.add(new LdapPrincipal(loginName));
+        this.loginSuccess = true;
+      }
+
+      final String loginDn = (String) this.sharedState.get(LOGIN_DN);
+      if (loginDn != null && this.setLdapDnPrincipal) {
+        this.principals.add(new LdapDnPrincipal(loginDn));
+        this.loginSuccess = true;
+      }
+
+      if (this.roleFilter != null) {
+        final Object[] filterArgs = new Object[] {loginDn, loginName, };
+        final Iterator<SearchResult> results = this.ldap.search(
+          new SearchFilter(this.roleFilter, filterArgs),
+          this.roleAttribute);
+        if (!results.hasNext() && this.noResultsIsError) {
+          this.loginSuccess = false;
+          throw new LoginException(
+            "Could not find roles using " + this.roleFilter);
+        }
+        while (results.hasNext()) {
+          final SearchResult sr = results.next();
+          this.roles.addAll(this.attributesToRoles(sr.getAttributes()));
+        }
+      }
+      if (this.defaultRole != null && !this.defaultRole.isEmpty()) {
+        this.roles.addAll(this.defaultRole);
+      }
+      if (!this.roles.isEmpty()) {
+        this.loginSuccess = true;
+      }
+      this.storeCredentials(nameCb, passCb, null);
+    } catch (NamingException e) {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Error occured attempting role lookup", e);
+      }
+      this.loginSuccess = false;
+      throw new LoginException(e.getMessage());
+    } finally {
+      this.ldap.close();
+    }
+    return true;
+  }
+
+
+  /**
+   * This provides command line access to a <code>LdapRoleLoginModule</code>.
+   *
+   * @param  args  <code>String[]</code>
+   *
+   * @throws  Exception  if an error occurs
+   */
+  public static void main(final String[] args)
+    throws Exception
+  {
+    String name = "vt-ldap-role";
+    if (args.length > 0) {
+      name = args[0];
+    }
+
+    final LoginContext lc = new LoginContext(name, new TextCallbackHandler());
+    lc.login();
+    System.out.println("Authorization succeeded");
+
+    final Set<Principal> principals = lc.getSubject().getPrincipals();
+    System.out.println("Subject Principal(s): ");
+
+    final Iterator<Principal> i = principals.iterator();
+    while (i.hasNext()) {
+      final Principal p = i.next();
+      System.out.println("  " + p.getName());
+    }
+    lc.logout();
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ldif/Ldif.java b/src/main/java/edu/vt/middleware/ldap/ldif/Ldif.java
new file mode 100644
index 0000000..2998f07
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ldif/Ldif.java
@@ -0,0 +1,398 @@
+/*
+  $Id: Ldif.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.ldif;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Iterator;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.LdapUtil;
+import edu.vt.middleware.ldap.bean.LdapAttribute;
+import edu.vt.middleware.ldap.bean.LdapBeanFactory;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>Ldif</code> contains functions for converting LDAP search result sets
+ * into LDIF.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class Ldif implements Serializable
+{
+
+  /** ASCII decimal value of nul. */
+  public static final int NUL_CHAR = 0;
+
+  /** ASCII decimal value of line feed. */
+  public static final int LF_CHAR = 10;
+
+  /** ASCII decimal value of carriage return. */
+  public static final int CR_CHAR = 13;
+
+  /** ASCII decimal value of space. */
+  public static final int SP_CHAR = 32;
+
+  /** ASCII decimal value of colon. */
+  public static final int COLON_CHAR = 58;
+
+  /** ASCII decimal value of left arrow. */
+  public static final int LA_CHAR = 60;
+
+  /** ASCII decimal value of highest character. */
+  public static final int MAX_ASCII_CHAR = 127;
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -3763879179455001975L;
+
+  /** Line separator. */
+  private static final String LINE_SEPARATOR = System.getProperty(
+    "line.separator");
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Ldap bean factory. */
+  protected LdapBeanFactory beanFactory = LdapBeanProvider.getLdapBeanFactory();
+
+
+  /**
+   * Returns the factory for creating ldap beans.
+   *
+   * @return  <code>LdapBeanFactory</code>
+   */
+  public LdapBeanFactory getLdapBeanFactory()
+  {
+    return this.beanFactory;
+  }
+
+
+  /**
+   * Sets the factory for creating ldap beans.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public void setLdapBeanFactory(final LdapBeanFactory lbf)
+  {
+    if (lbf != null) {
+      this.beanFactory = lbf;
+    }
+  }
+
+
+  /**
+   * This will take the results of a prior LDAP query and convert it to LDIF.
+   *
+   * @param  results  <code>Iterator</code> of LDAP search results
+   *
+   * @return  <code>String</code>
+   */
+  public String createLdif(final Iterator<SearchResult> results)
+  {
+    String ldif = "";
+    try {
+      final LdapResult lr = this.beanFactory.newLdapResult();
+      lr.addEntries(results);
+      ldif = this.createLdif(lr);
+    } catch (NamingException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error creating String from SearchResults", e);
+      }
+    }
+    return ldif;
+  }
+
+
+  /**
+   * This will take the results of a prior LDAP query and convert it to LDIF.
+   *
+   * @param  result  <code>LdapResult</code>
+   *
+   * @return  <code>String</code>
+   */
+  public String createLdif(final LdapResult result)
+  {
+    // build string from results
+    final StringBuffer ldif = new StringBuffer();
+    if (result != null) {
+      for (LdapEntry le : result.getEntries()) {
+        ldif.append(createLdifEntry(le));
+      }
+    }
+
+    return ldif.toString();
+  }
+
+
+  /**
+   * This will take an LDAP entry and convert it to LDIF.
+   *
+   * @param  ldapEntry  <code>LdapEntry</code> to convert
+   *
+   * @return  <code>String</code>
+   */
+  protected String createLdifEntry(final LdapEntry ldapEntry)
+  {
+    final StringBuffer entry = new StringBuffer();
+    if (ldapEntry != null) {
+
+      final String dn = ldapEntry.getDn();
+      if (dn != null) {
+        if (encodeData(dn)) {
+          final String encodedDn = LdapUtil.base64Encode(dn);
+          if (encodedDn != null) {
+            entry.append("dn:: ").append(dn).append(LINE_SEPARATOR);
+          }
+        } else {
+          entry.append("dn: ").append(dn).append(LINE_SEPARATOR);
+        }
+      }
+
+      for (LdapAttribute attr : ldapEntry.getLdapAttributes().getAttributes()) {
+        final String attrName = attr.getName();
+        for (Object attrValue : attr.getValues()) {
+          if (encodeData(attrValue)) {
+            String encodedAttrValue = null;
+            if (attrValue instanceof String) {
+              encodedAttrValue = LdapUtil.base64Encode((String) attrValue);
+            } else if (attrValue instanceof byte[]) {
+              encodedAttrValue = LdapUtil.base64Encode((byte[]) attrValue);
+            } else {
+              if (this.logger.isWarnEnabled()) {
+                this.logger.warn(
+                  "Could not cast attribute value as a byte[]" +
+                  " or a String");
+              }
+            }
+            if (encodedAttrValue != null) {
+              entry.append(attrName).append(":: ").append(encodedAttrValue)
+                .append(LINE_SEPARATOR);
+            }
+          } else {
+            entry.append(attrName).append(": ").append(attrValue).append(
+              LINE_SEPARATOR);
+          }
+        }
+      }
+    }
+
+    if (entry.length() > 0) {
+      entry.append(LINE_SEPARATOR);
+    }
+    return entry.toString();
+  }
+
+
+  /**
+   * This determines whether the supplied data should be base64 encoded. See
+   * http://www.faqs.org/rfcs/rfc2849.html for more details.
+   *
+   * @param  data  <code>Object</code> to inspect
+   *
+   * @return  <code>boolean</code>
+   */
+  private boolean encodeData(final Object data)
+  {
+    boolean encode = false;
+    if (data instanceof String) {
+      final String stringData = (String) data;
+      final char[] dataCharArray = stringData.toCharArray();
+      for (int i = 0; i < dataCharArray.length; i++) {
+        final int charInt = (int) dataCharArray[i];
+        // check for NUL
+        if (charInt == NUL_CHAR) {
+          encode = true;
+          // check for LF
+        } else if (charInt == LF_CHAR) {
+          encode = true;
+          // check for CR
+        } else if (charInt == CR_CHAR) {
+          encode = true;
+          // check for SP at beginning or end of string
+        } else if (
+          charInt == SP_CHAR &&
+            (i == 0 || i == dataCharArray.length - 1)) {
+          encode = true;
+          // check for colon(:) at beginning of string
+        } else if (charInt == COLON_CHAR && i == 0) {
+          encode = true;
+          // check for left arrow(<) at beginning of string
+        } else if (charInt == LA_CHAR && i == 0) {
+          encode = true;
+          // check for any character above 127
+        } else if (charInt > MAX_ASCII_CHAR) {
+          encode = true;
+        }
+      }
+    } else {
+      encode = true;
+    }
+    return encode;
+  }
+
+
+  /**
+   * This will write the supplied LDAP search results to the supplied writer in
+   * LDIF form.
+   *
+   * @param  results  <code>Iterator</code> of LDAP search results
+   * @param  writer  <code>Writer</code> to write to
+   *
+   * @throws  IOException  if an error occurs while writing to the output stream
+   */
+  public void outputLdif(
+    final Iterator<SearchResult> results,
+    final Writer writer)
+    throws IOException
+  {
+    writer.write(createLdif(results));
+    writer.flush();
+  }
+
+
+  /**
+   * This will write the supplied LDAP search results to the supplied writer in
+   * LDIF form.
+   *
+   * @param  result  <code>LdapResult</code>
+   * @param  writer  <code>Writer</code> to write to
+   *
+   * @throws  IOException  if an error occurs while writing to the output stream
+   */
+  public void outputLdif(final LdapResult result, final Writer writer)
+    throws IOException
+  {
+    writer.write(createLdif(result));
+    writer.flush();
+  }
+
+
+  /**
+   * This will take a Reader containing an LDIF and convert it to an Iterator of
+   * LDAP search results. Provides a loose implementation of RFC 2849. Should
+   * not be used to validate LDIF format as it does not enforce strictness.
+   *
+   * @param  reader  <code>Reader</code> containing LDIF content
+   *
+   * @return  <code>Iterator</code> - of LDAP search results
+   *
+   * @throws  IOException  if an I/O error occurs
+   */
+  public Iterator<SearchResult> importLdif(final Reader reader)
+    throws IOException
+  {
+    return this.importLdifToLdapResult(reader).toSearchResults().iterator();
+  }
+
+
+  /**
+   * This will take a Reader containing an LDIF and convert it to an <code>
+   * LdapResult</code>. Provides a loose implementation of RFC 2849. Should not
+   * be used to validate LDIF format as it does not enforce strictness.
+   *
+   * @param  reader  <code>Reader</code> containing LDIF content
+   *
+   * @return  <code>LdapResult</code> - LDAP search results
+   *
+   * @throws  IOException  if an I/O error occurs
+   */
+  public LdapResult importLdifToLdapResult(final Reader reader)
+    throws IOException
+  {
+    final LdapResult ldapResult = this.beanFactory.newLdapResult();
+    final BufferedReader br = new BufferedReader(reader);
+    String line = null;
+    int lineCount = 0;
+    LdapEntry ldapEntry = null;
+    StringBuffer lineValue = new StringBuffer();
+
+    while ((line = br.readLine()) != null) {
+      lineCount++;
+      if (line.startsWith("dn:")) {
+        lineValue.append(line);
+        ldapEntry = this.beanFactory.newLdapEntry();
+        break;
+      }
+    }
+
+    boolean read = true;
+    while (read) {
+      line = br.readLine();
+      if (line == null) {
+        read = false;
+        line = "";
+      }
+      if (!line.startsWith("#")) {
+        if (line.startsWith("dn:")) {
+          ldapResult.addEntry(ldapEntry);
+          ldapEntry = this.beanFactory.newLdapEntry();
+        }
+        if (line.startsWith(" ")) {
+          lineValue.append(line.substring(1));
+        } else {
+          final String s = lineValue.toString();
+          if (s.indexOf(":") != -1) {
+            boolean isBinary = false;
+            boolean isUrl = false;
+            final String[] parts = s.split(":", 2);
+            final String attrName = parts[0];
+            String attrValue = parts[1];
+            if (attrValue.startsWith(":")) {
+              isBinary = true;
+              attrValue = attrValue.substring(1);
+            } else if (attrValue.startsWith("<")) {
+              isUrl = true;
+              attrValue = attrValue.substring(1);
+            }
+            if (attrValue.startsWith(" ")) {
+              attrValue = attrValue.substring(1);
+            }
+            if ("dn".equals(attrName)) {
+              ldapEntry.setDn(attrValue);
+            } else {
+              LdapAttribute ldapAttr = ldapEntry.getLdapAttributes()
+                  .getAttribute(attrName);
+              if (ldapAttr == null) {
+                ldapAttr = this.beanFactory.newLdapAttribute();
+                ldapAttr.setName(attrName);
+                ldapEntry.getLdapAttributes().addAttribute(ldapAttr);
+              }
+              if (isBinary) {
+                ldapAttr.getValues().add(LdapUtil.base64Decode(attrValue));
+              } else if (isUrl) {
+                ldapAttr.getValues().add(LdapUtil.readURL(new URL(attrValue)));
+              } else {
+                ldapAttr.getValues().add(attrValue);
+              }
+            }
+          }
+          lineValue = new StringBuffer(line);
+        }
+      }
+    }
+    if (ldapEntry != null) {
+      ldapResult.addEntry(ldapEntry);
+    }
+    return ldapResult;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ldif/LdifResultConverter.java b/src/main/java/edu/vt/middleware/ldap/ldif/LdifResultConverter.java
new file mode 100644
index 0000000..da6d522
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ldif/LdifResultConverter.java
@@ -0,0 +1,116 @@
+/*
+  $Id: LdifResultConverter.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.ldif;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.bean.LdapBeanFactory;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>LdifResultConverter</code> provides utility methods for converting
+ * <code>LdapResult</code> to and from LDIF in string format.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdifResultConverter
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(getClass());
+
+  /** Ldap bean factory. */
+  protected LdapBeanFactory beanFactory = LdapBeanProvider.getLdapBeanFactory();
+
+  /** Class for outputting LDIF. */
+  private Ldif ldif = new Ldif();
+
+
+  /**
+   * Returns the factory for creating ldap beans.
+   *
+   * @return  <code>LdapBeanFactory</code>
+   */
+  public LdapBeanFactory getLdapBeanFactory()
+  {
+    return this.beanFactory;
+  }
+
+
+  /**
+   * Sets the factory for creating ldap beans.
+   *
+   * @param  lbf  <code>LdapBeanFactory</code>
+   */
+  public void setLdapBeanFactory(final LdapBeanFactory lbf)
+  {
+    if (lbf != null) {
+      this.beanFactory = lbf;
+      this.ldif.setLdapBeanFactory(lbf);
+    }
+  }
+
+
+  /**
+   * This returns this <code>LdifResult</code> as LDIF.
+   *
+   * @param  result  <code>LdapResult</code> to convert
+   *
+   * @return  <code>String</code>
+   */
+  public String toLdif(final LdapResult result)
+  {
+    final StringWriter writer = new StringWriter();
+    try {
+      this.ldif.outputLdif(result.toSearchResults().iterator(), writer);
+    } catch (IOException e) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("Could not write ldif to StringWriter", e);
+      }
+    }
+    return writer.toString();
+  }
+
+
+  /**
+   * This reads any entries in the supplied LDIF into this <code>
+   * LdifResult</code>.
+   *
+   * @param  ldif  <code>String</code> to read
+   *
+   * @return  <code>LdapResult</code>
+   */
+  public LdapResult fromLdif(final String ldif)
+  {
+    final LdapResult result = this.beanFactory.newLdapResult();
+    try {
+      result.addEntries(this.ldif.importLdif(new StringReader(ldif)));
+    } catch (IOException e) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("Could not read ldif from StringReader", e);
+      }
+    } catch (NamingException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Unexpected naming exception occurred", e);
+      }
+    }
+    return result;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ldif/LdifSearch.java b/src/main/java/edu/vt/middleware/ldap/ldif/LdifSearch.java
new file mode 100644
index 0000000..5732833
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ldif/LdifSearch.java
@@ -0,0 +1,69 @@
+/*
+  $Id: LdifSearch.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.ldif;
+
+import java.io.IOException;
+import java.io.Writer;
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapSearch;
+import edu.vt.middleware.ldap.pool.LdapPool;
+
+/**
+ * <code>LdifSearch</code> queries an LDAP and returns the result as an LDIF.
+ * Each instance of <code>LdifSearch</code> maintains it's own pool of LDAP
+ * connections.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdifSearch extends LdapSearch
+{
+
+  /** Ldif object. */
+  private Ldif ldif = new Ldif();
+
+
+  /**
+   * This creates a new <code>LdifSearch</code> with the supplied pool.
+   *
+   * @param  pool  <code>LdapPool</code>
+   */
+  public LdifSearch(final LdapPool<Ldap> pool)
+  {
+    super(pool);
+  }
+
+
+  /**
+   * This will perform an LDAP search with the supplied query and return
+   * attributes. The results will be written to the supplied <code>
+   * Writer</code>.
+   *
+   * @param  query  <code>String</code> to search for
+   * @param  attrs  <code>String[]</code> to return
+   * @param  writer  <code>Writer</code> to write to
+   *
+   * @throws  NamingException  if an error occurs while searching
+   * @throws  IOException  if an error occurs while writing search results
+   */
+  public void search(
+    final String query,
+    final String[] attrs,
+    final Writer writer)
+    throws NamingException, IOException
+  {
+    this.ldif.outputLdif(this.search(query, attrs), writer);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/AbstractLdapFactory.java b/src/main/java/edu/vt/middleware/ldap/pool/AbstractLdapFactory.java
new file mode 100644
index 0000000..b24aade
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/AbstractLdapFactory.java
@@ -0,0 +1,175 @@
+/*
+  $Id: AbstractLdapFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import edu.vt.middleware.ldap.BaseLdap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractLdapFactory</code> provides a basic implementation of an ldap
+ * factory.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class AbstractLdapFactory<T extends BaseLdap>
+  implements LdapFactory<T>
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** For activating ldap objects. */
+  protected LdapActivator<T> activator;
+
+  /** For passivating ldap objects. */
+  protected LdapPassivator<T> passivator;
+
+  /** For validating ldap objects. */
+  protected LdapValidator<T> validator;
+
+
+  /**
+   * Sets the ldap activator for this factory.
+   *
+   * @param  la  ldap activator
+   */
+  public void setLdapActivator(final LdapActivator<T> la)
+  {
+    this.activator = la;
+  }
+
+
+  /**
+   * Returns the ldap activator for this factory.
+   *
+   * @return  ldap activator
+   */
+  public LdapActivator<T> getLdapActivator()
+  {
+    return this.activator;
+  }
+
+
+  /**
+   * Sets the ldap passivator for this factory.
+   *
+   * @param  lp  ldap passivator
+   */
+  public void setLdapPassivator(final LdapPassivator<T> lp)
+  {
+    this.passivator = lp;
+  }
+
+
+  /**
+   * Returns the ldap passivator for this factory.
+   *
+   * @return  ldap passivator
+   */
+  public LdapPassivator<T> getLdapPassivator()
+  {
+    return this.passivator;
+  }
+
+
+  /**
+   * Sets the ldap validator for this factory.
+   *
+   * @param  lv  ldap validator
+   */
+  public void setLdapValidator(final LdapValidator<T> lv)
+  {
+    this.validator = lv;
+  }
+
+
+  /**
+   * Returns the ldap validator for this factory.
+   *
+   * @return  ldap validator
+   */
+  public LdapValidator<T> getLdapValidator()
+  {
+    return this.validator;
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract T create();
+
+
+  /** {@inheritDoc} */
+  public abstract void destroy(final T t);
+
+
+  /** {@inheritDoc} */
+  public boolean activate(final T t)
+  {
+    boolean success = false;
+    if (this.activator == null) {
+      success = true;
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("no activator configured");
+      }
+    } else {
+      success = this.activator.activate(t);
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("activation for " + t + " = " + success);
+      }
+    }
+    return success;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean passivate(final T t)
+  {
+    boolean success = false;
+    if (this.passivator == null) {
+      success = true;
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("no passivator configured");
+      }
+    } else {
+      success = this.passivator.passivate(t);
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("passivation for " + t + " = " + success);
+      }
+    }
+    return success;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean validate(final T t)
+  {
+    boolean success = false;
+    if (this.validator == null) {
+      success = true;
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("validate called, but no validator configured");
+      }
+    } else {
+      success = this.validator.validate(t);
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("validation for " + t + " = " + success);
+      }
+    }
+    return success;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/AbstractLdapPool.java b/src/main/java/edu/vt/middleware/ldap/pool/AbstractLdapPool.java
new file mode 100644
index 0000000..61a422c
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/AbstractLdapPool.java
@@ -0,0 +1,664 @@
+/*
+  $Id: AbstractLdapPool.java 2241 2012-02-07 20:08:51Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2241 $
+  Updated: $Date: 2012-02-07 20:08:51 +0000 (Tue, 07 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.Timer;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+import edu.vt.middleware.ldap.BaseLdap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractLdapPool</code> contains the basic implementation for pooling
+ * ldap objects. The main design objective for the supplied pooling
+ * implementations is to provide a pool that does not block on object creation
+ * or destruction. This is what accounts for the multiple locks available on
+ * this class. The pool is backed by two queues, one for available objects and
+ * one for active objects. Objects that are available for {@link #checkOut()}
+ * exist in the available queue. Objects that are actively in use exist in the
+ * active queue. Note that depending on the implementation an object can exist
+ * in both queues at the same time.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2241 $ $Date: 2012-02-07 20:08:51 +0000 (Tue, 07 Feb 2012) $
+ */
+public abstract class AbstractLdapPool<T extends BaseLdap>
+  implements LdapPool<T>
+{
+
+  /** Lock for the entire pool. */
+  protected final ReentrantLock poolLock = new ReentrantLock();
+
+  /** Condition for notifying threads that an object was returned. */
+  protected final Condition poolNotEmpty = poolLock.newCondition();
+
+  /** Lock for check ins. */
+  protected final ReentrantLock checkInLock = new ReentrantLock();
+
+  /** Lock for check outs. */
+  protected final ReentrantLock checkOutLock = new ReentrantLock();
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** List of available ldap objects in the pool. */
+  protected Queue<PooledLdap<T>> available = new LinkedList<PooledLdap<T>>();
+
+  /** List of ldap objects in use. */
+  protected Queue<PooledLdap<T>> active = new LinkedList<PooledLdap<T>>();
+
+  /** Ldap pool config. */
+  protected LdapPoolConfig poolConfig;
+
+  /** Factory to create ldap objects. */
+  protected LdapFactory<T> ldapFactory;
+
+  /** Timer for scheduling pool tasks. */
+  private Timer poolTimer = new Timer(true);
+
+
+  /**
+   * Creates a new pool with the supplied pool configuration and ldap factory.
+   * The pool configuration will be marked as immutable by this pool.
+   *
+   * @param  lpc  <code>LdapPoolConfig</code>
+   * @param  lf  <code>LdapFactory</code>
+   */
+  public AbstractLdapPool(final LdapPoolConfig lpc, final LdapFactory<T> lf)
+  {
+    this.poolConfig = lpc;
+    this.poolConfig.makeImmutable();
+    this.ldapFactory = lf;
+  }
+
+
+  /** {@inheritDoc} */
+  public LdapPoolConfig getLdapPoolConfig()
+  {
+    return this.poolConfig;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setPoolTimer(final Timer t)
+  {
+    this.poolTimer = t;
+  }
+
+
+  /** {@inheritDoc} */
+  public void initialize()
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("beginning pool initialization");
+    }
+
+    this.poolTimer.scheduleAtFixedRate(
+      new PrunePoolTask<T>(this),
+      this.poolConfig.getPruneTimerPeriod(),
+      this.poolConfig.getPruneTimerPeriod());
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("prune pool task scheduled");
+    }
+
+    this.poolTimer.scheduleAtFixedRate(
+      new ValidatePoolTask<T>(this),
+      this.poolConfig.getValidateTimerPeriod(),
+      this.poolConfig.getValidateTimerPeriod());
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("validate pool task scheduled");
+    }
+
+    this.initializePool();
+
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("pool initialized to size " + this.available.size());
+    }
+  }
+
+
+  /**
+   * Attempts to fill the pool to its minimum size.
+   *
+   * @throws  IllegalStateException  if the pool does not contain at least one
+   * connection and it's minimum size is greater than zero
+   */
+  private void initializePool()
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        "checking ldap pool size >= " + this.poolConfig.getMinPoolSize());
+    }
+
+    int count = 0;
+    this.poolLock.lock();
+    try {
+      while (
+        this.available.size() < this.poolConfig.getMinPoolSize() &&
+          count < this.poolConfig.getMinPoolSize() * 2) {
+        final T t = this.createAvailable();
+        if (this.poolConfig.isValidateOnCheckIn()) {
+          if (this.ldapFactory.validate(t)) {
+            if (this.logger.isTraceEnabled()) {
+              this.logger.trace(
+                "ldap object passed initialize validation: " + t);
+            }
+          } else {
+            if (this.logger.isWarnEnabled()) {
+              this.logger.warn(
+                "ldap object failed initialize validation: " + t);
+            }
+            this.removeAvailable(t);
+          }
+        }
+        count++;
+      }
+      if (this.available.size() == 0 && this.poolConfig.getMinPoolSize() > 0) {
+        throw new IllegalStateException("Could not initialize pool");
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void close()
+  {
+    this.poolLock.lock();
+    try {
+      while (this.available.size() > 0) {
+        final PooledLdap<T> pl = this.available.remove();
+        this.ldapFactory.destroy(pl.getLdap());
+      }
+      while (this.active.size() > 0) {
+        final PooledLdap<T> pl = this.active.remove();
+        this.ldapFactory.destroy(pl.getLdap());
+      }
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("pool closed");
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+
+    this.poolTimer.cancel();
+  }
+
+
+  /**
+   * Create a new ldap object and place it in the available pool.
+   *
+   * @return  ldap object that was placed in the available pool
+   */
+  protected T createAvailable()
+  {
+    final T t = this.ldapFactory.create();
+    if (t != null) {
+      final PooledLdap<T> pl = new PooledLdap<T>(t);
+      this.poolLock.lock();
+      try {
+        this.available.add(pl);
+      } finally {
+        this.poolLock.unlock();
+      }
+    } else {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("unable to create available ldap object");
+      }
+    }
+    return t;
+  }
+
+
+  /**
+   * Create a new ldap object and place it in the active pool.
+   *
+   * @return  ldap object that was placed in the active pool
+   */
+  protected T createActive()
+  {
+    final T t = this.ldapFactory.create();
+    if (t != null) {
+      final PooledLdap<T> pl = new PooledLdap<T>(t);
+      this.poolLock.lock();
+      try {
+        this.active.add(pl);
+      } finally {
+        this.poolLock.unlock();
+      }
+    } else {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("unable to create active ldap object");
+      }
+    }
+    return t;
+  }
+
+
+  /**
+   * Create a new ldap object and place it in both the available and active
+   * pools.
+   *
+   * @return  ldap object that was placed in the available and active pools
+   */
+  protected T createAvailableAndActive()
+  {
+    final T t = this.ldapFactory.create();
+    if (t != null) {
+      final PooledLdap<T> pl = new PooledLdap<T>(t);
+      this.poolLock.lock();
+      try {
+        this.available.add(pl);
+        this.active.add(pl);
+      } finally {
+        this.poolLock.unlock();
+      }
+    } else {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("unable to create available and active ldap object");
+      }
+    }
+    return t;
+  }
+
+
+  /**
+   * Remove an ldap object from the available pool.
+   *
+   * @param  t  ldap object that exists in the available pool
+   */
+  protected void removeAvailable(final T t)
+  {
+    boolean destroy = false;
+    final PooledLdap<T> pl = new PooledLdap<T>(t);
+    this.poolLock.lock();
+    try {
+      if (this.available.remove(pl)) {
+        destroy = true;
+      } else {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn(
+            "attempt to remove unknown available ldap object: " + t);
+        }
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+    if (destroy) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("removing available ldap object: " + t);
+      }
+      this.ldapFactory.destroy(t);
+    }
+  }
+
+
+  /**
+   * Remove an ldap object from the active pool.
+   *
+   * @param  t  ldap object that exists in the active pool
+   */
+  protected void removeActive(final T t)
+  {
+    boolean destroy = false;
+    final PooledLdap<T> pl = new PooledLdap<T>(t);
+    this.poolLock.lock();
+    try {
+      if (this.active.remove(pl)) {
+        destroy = true;
+      } else {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn(
+            "attempt to remove unknown active ldap object: " + t);
+        }
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+    if (destroy) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("removing active ldap object: " + t);
+      }
+      this.ldapFactory.destroy(t);
+    }
+  }
+
+
+  /**
+   * Remove an ldap object from both the available and active pools.
+   *
+   * @param  t  ldap object that exists in the both the available and active
+   * pools
+   */
+  protected void removeAvailableAndActive(final T t)
+  {
+    boolean destroy = false;
+    final PooledLdap<T> pl = new PooledLdap<T>(t);
+    this.poolLock.lock();
+    try {
+      if (this.available.remove(pl)) {
+        destroy = true;
+      } else {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug(
+            "attempt to remove unknown available ldap object: " + t);
+        }
+      }
+      if (this.active.remove(pl)) {
+        destroy = true;
+      } else {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug(
+            "attempt to remove unknown active ldap object: " + t);
+        }
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+    if (destroy) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("removing active ldap object: " + t);
+      }
+      this.ldapFactory.destroy(t);
+    }
+  }
+
+
+  /**
+   * Attempts to activate and validate an ldap object. Performed before an
+   * object is returned from {@link LdapPool#checkOut()}.
+   *
+   * @param  t  ldap object
+   *
+   * @throws  LdapPoolException  if this method fais
+   * @throws  LdapActivationException  if the ldap object cannot be activated
+   * @throws  LdapValidateException  if the ldap object cannot be validated
+   */
+  protected void activateAndValidate(final T t)
+    throws LdapPoolException
+  {
+    if (!this.ldapFactory.activate(t)) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("ldap object failed activation: " + t);
+      }
+      this.removeAvailableAndActive(t);
+      throw new LdapActivationException("Activation of ldap object failed");
+    }
+    if (
+      this.poolConfig.isValidateOnCheckOut() &&
+        !this.ldapFactory.validate(t)) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("ldap object failed check out validation: " + t);
+      }
+      this.removeAvailableAndActive(t);
+      throw new LdapValidationException("Validation of ldap object failed");
+    }
+  }
+
+
+  /**
+   * Attempts to validate and passivate an ldap object. Performed when an object
+   * is given to {@link LdapPool#checkIn}.
+   *
+   * @param  t  ldap object
+   *
+   * @return  whether both validate and passivation succeeded
+   */
+  protected boolean validateAndPassivate(final T t)
+  {
+    boolean valid = false;
+    if (this.poolConfig.isValidateOnCheckIn()) {
+      if (!this.ldapFactory.validate(t)) {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn("ldap object failed check in validation: " + t);
+        }
+      } else {
+        valid = true;
+      }
+    } else {
+      valid = true;
+    }
+    if (valid && !this.ldapFactory.passivate(t)) {
+      valid = false;
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("ldap object failed activation: " + t);
+      }
+    }
+    return valid;
+  }
+
+
+  /** {@inheritDoc} */
+  public void prune()
+  {
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting for pool lock to prune " + this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      if (this.active.size() == 0) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("pruning pool of size " + this.available.size());
+        }
+        while (this.available.size() > this.poolConfig.getMinPoolSize()) {
+          PooledLdap<T> pl = this.available.peek();
+          final long time = System.currentTimeMillis() - pl.getCreatedTime();
+          if (time > this.poolConfig.getExpirationTime()) {
+            pl = this.available.remove();
+            if (this.logger.isTraceEnabled()) {
+              this.logger.trace(
+                "removing " + pl.getLdap() + " in the pool for " + time + "ms");
+            }
+            this.ldapFactory.destroy(pl.getLdap());
+          } else {
+            break;
+          }
+        }
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("pool size pruned to " + this.available.size());
+        }
+      } else {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("pool is currently active, no objects pruned");
+        }
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void validate()
+  {
+    this.poolLock.lock();
+    try {
+      if (this.active.size() == 0) {
+        if (this.poolConfig.isValidatePeriodically()) {
+          if (this.logger.isDebugEnabled()) {
+            this.logger.debug(
+              "validate for pool of size " + this.available.size());
+          }
+
+          final Queue<PooledLdap<T>> remove = new LinkedList<PooledLdap<T>>();
+          for (PooledLdap<T> pl : this.available) {
+            if (this.logger.isTraceEnabled()) {
+              this.logger.trace("validating " + pl.getLdap());
+            }
+            if (this.ldapFactory.validate(pl.getLdap())) {
+              if (this.logger.isTraceEnabled()) {
+                this.logger.trace(
+                  "ldap object passed validation: " + pl.getLdap());
+              }
+            } else {
+              if (this.logger.isWarnEnabled()) {
+                this.logger.warn(
+                  "ldap object failed validation: " + pl.getLdap());
+              }
+              remove.add(pl);
+            }
+          }
+          for (PooledLdap<T> pl : remove) {
+            if (this.logger.isTraceEnabled()) {
+              this.logger.trace("removing " + pl.getLdap() + " from the pool");
+            }
+            this.available.remove(pl);
+            this.ldapFactory.destroy(pl.getLdap());
+          }
+        }
+        this.initializePool();
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug(
+            "pool size after validation is " + this.available.size());
+        }
+      } else {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug(
+            "pool is currently active, " +
+            "no validation performed");
+        }
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public int availableCount()
+  {
+    return this.available.size();
+  }
+
+
+  /** {@inheritDoc} */
+  public int activeCount()
+  {
+    return this.active.size();
+  }
+
+
+  /**
+   * 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();
+    }
+  }
+
+
+  /**
+   * <code>PooledLdap</code> contains an ldap object that is participating in a
+   * pool. Used to track how long an ldap object has been in either the
+   * available or active queues.
+   *
+   * @param  <T>  type of ldap object
+   */
+  static protected class PooledLdap<T extends BaseLdap>
+  {
+
+    /** hash code seed. */
+    protected static final int HASH_CODE_SEED = 89;
+
+    /** Underlying ldap object. */
+    private T ldap;
+
+    /** Time this object was created. */
+    private long createdTime;
+
+
+    /**
+     * Creates a new <code>PooledLdap</code> with the supplied ldap object.
+     *
+     * @param  t  ldap object
+     */
+    public PooledLdap(final T t)
+    {
+      this.ldap = t;
+      this.createdTime = System.currentTimeMillis();
+    }
+
+
+    /**
+     * Returns the ldap object.
+     *
+     * @return  underlying ldap object
+     */
+    public T getLdap()
+    {
+      return this.ldap;
+    }
+
+
+    /**
+     * Returns the time this object was created.
+     *
+     * @return  creation time
+     */
+    public long getCreatedTime()
+    {
+      return this.createdTime;
+    }
+
+
+    /**
+     * Returns whether the supplied <code>Object</code> contains the same data
+     * as this bean.
+     *
+     * @param  o  <code>Object</code>
+     *
+     * @return  <code>boolean</code>
+     */
+    public boolean equals(final Object o)
+    {
+      if (o == null) {
+        return false;
+      }
+      return
+        o == this ||
+          (this.getClass() == o.getClass() &&
+            o.hashCode() == this.hashCode());
+    }
+
+
+    /**
+     * This returns the hash code for this object.
+     *
+     * @return  <code>int</code>
+     */
+    public int hashCode()
+    {
+      int hc = HASH_CODE_SEED;
+      if (this.ldap != null) {
+        hc += this.ldap.hashCode();
+      }
+      return hc;
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/BlockingLdapPool.java b/src/main/java/edu/vt/middleware/ldap/pool/BlockingLdapPool.java
new file mode 100644
index 0000000..b42638f
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/BlockingLdapPool.java
@@ -0,0 +1,323 @@
+/*
+  $Id: BlockingLdapPool.java 2241 2012-02-07 20:08:51Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2241 $
+  Updated: $Date: 2012-02-07 20:08:51 +0000 (Tue, 07 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.util.NoSuchElementException;
+import java.util.concurrent.TimeUnit;
+import edu.vt.middleware.ldap.Ldap;
+
+/**
+ * <code>BlockingLdapPool</code> implements a pool of ldap objects that has a
+ * set minimum and maximum size. The pool will not grow beyond the maximum size
+ * and when the pool is exhausted, requests for new objects will block. The
+ * length of time the pool will block is determined by {@link
+ * #getBlockWaitTime()}. By default the pool will block indefinitely and there
+ * is no guarantee that waiting threads will be serviced in the order in which
+ * they made their request. This implementation should be used when you need to
+ * control the <em>exact</em> number of ldap connections that can be created.
+ * See {@link AbstractLdapPool}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2241 $ $Date: 2012-02-07 20:08:51 +0000 (Tue, 07 Feb 2012) $
+ */
+public class BlockingLdapPool extends AbstractLdapPool<Ldap>
+{
+
+  /** Time in milliseconds to wait for an available ldap object. */
+  private long blockWaitTime;
+
+
+  /** Creates a new ldap pool using {@link DefaultLdapFactory}. */
+  public BlockingLdapPool()
+  {
+    super(new LdapPoolConfig(), new DefaultLdapFactory());
+  }
+
+
+  /**
+   * Creates a new ldap pool with the supplied ldap factory.
+   *
+   * @param  lf  ldap factory
+   */
+  public BlockingLdapPool(final LdapFactory<Ldap> lf)
+  {
+    super(new LdapPoolConfig(), lf);
+  }
+
+
+  /**
+   * Creates a new ldap pool with the supplied ldap config and factory.
+   *
+   * @param  lpc  ldap pool configuration
+   * @param  lf  ldap factory
+   */
+  public BlockingLdapPool(final LdapPoolConfig lpc, final LdapFactory<Ldap> lf)
+  {
+    super(lpc, lf);
+  }
+
+
+  /**
+   * Returns the block wait time. Default time is 0, which will wait
+   * indefinitely.
+   *
+   * @return  time in milliseconds to wait for available ldap objects
+   */
+  public long getBlockWaitTime()
+  {
+    return this.blockWaitTime;
+  }
+
+
+  /**
+   * Sets the block wait time. Default time is 0, which will wait indefinitely.
+   *
+   * @param  time  in milliseconds to wait for available ldap objects
+   */
+  public void setBlockWaitTime(final long time)
+  {
+    if (time >= 0) {
+      this.blockWaitTime = time;
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public Ldap checkOut()
+    throws LdapPoolException
+  {
+    Ldap l = null;
+    boolean create = false;
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting on pool lock for check out " + this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      // if an available object exists, use it
+      // if no available objects and the pool can grow, attempt to create
+      // otherwise the pool is full, block until an object is returned
+      if (this.available.size() > 0) {
+        try {
+          if (this.logger.isTraceEnabled()) {
+            this.logger.trace("retrieve available ldap object");
+          }
+          l = this.retrieveAvailable();
+        } catch (NoSuchElementException e) {
+          if (this.logger.isErrorEnabled()) {
+            this.logger.error("could not remove ldap object from list", e);
+          }
+          throw new IllegalStateException("Pool is empty", e);
+        }
+      } else if (this.active.size() < this.poolConfig.getMaxPoolSize()) {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("pool can grow, attempt to create ldap object");
+        }
+        create = true;
+      } else {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace(
+            "pool is full, block until ldap object " +
+            "is available");
+        }
+        l = this.blockAvailable();
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+
+    if (create) {
+      // previous block determined a creation should occur
+      // block here until create occurs without locking the whole pool
+      // if the pool is already maxed or creates are failing,
+      // block until an object is available
+      this.checkOutLock.lock();
+      try {
+        boolean b = true;
+        this.poolLock.lock();
+        try {
+          if (
+            this.available.size() + this.active.size() ==
+              this.poolConfig.getMaxPoolSize()) {
+            b = false;
+          }
+        } finally {
+          this.poolLock.unlock();
+        }
+        if (b) {
+          l = this.createActive();
+          if (this.logger.isTraceEnabled()) {
+            this.logger.trace("created new active ldap object: " + l);
+          }
+        }
+      } finally {
+        this.checkOutLock.unlock();
+      }
+      if (l == null) {
+        if (this.available.size() == 0 && this.active.size() == 0) {
+          if (this.logger.isErrorEnabled()) {
+            this.logger.error("Could not service check out request");
+          }
+          throw new LdapPoolExhaustedException(
+            "Pool is empty and object creation failed");
+        }
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug(
+            "create failed, block until ldap object " +
+            "is available");
+        }
+        l = this.blockAvailable();
+      }
+    }
+
+    if (l != null) {
+      this.activateAndValidate(l);
+    } else {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Could not service check out request");
+      }
+      throw new LdapPoolExhaustedException(
+        "Pool is empty and object creation failed");
+    }
+
+    return l;
+  }
+
+
+  /**
+   * This attempts to retrieve an ldap object from the available queue.
+   *
+   * @return  ldap object from the pool
+   *
+   * @throws  NoSuchElementException  if the available queue is empty
+   */
+  protected Ldap retrieveAvailable()
+  {
+    Ldap l = null;
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting on pool lock for retrieve available " +
+        this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      final PooledLdap<Ldap> pl = this.available.remove();
+      this.active.add(new PooledLdap<Ldap>(pl.getLdap()));
+      l = pl.getLdap();
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("retrieved available ldap object: " + l);
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+    return l;
+  }
+
+
+  /**
+   * This blocks until an ldap object can be aquired.
+   *
+   * @return  ldap object from the pool
+   *
+   * @throws  LdapPoolException  if this method fails
+   * @throws  BlockingTimeoutException  if this pool is configured with a block
+   * time and it occurs
+   * @throws  PoolInterruptedException  if the current thread is interrupted
+   */
+  protected Ldap blockAvailable()
+    throws LdapPoolException
+  {
+    Ldap l = null;
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting on pool lock for block available " +
+        this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      while (l == null) {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("available pool is empty, waiting...");
+        }
+        if (this.blockWaitTime > 0) {
+          if (
+            !this.poolNotEmpty.await(
+                this.blockWaitTime,
+                TimeUnit.MILLISECONDS)) {
+            if (this.logger.isDebugEnabled()) {
+              this.logger.debug("block time exceeded, throwing exception");
+            }
+            throw new BlockingTimeoutException("Block time exceeded");
+          }
+        } else {
+          this.poolNotEmpty.await();
+        }
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("notified to continue...");
+        }
+        try {
+          l = this.retrieveAvailable();
+        } catch (NoSuchElementException e) {
+          if (this.logger.isTraceEnabled()) {
+            this.logger.trace("notified to continue but pool was empty");
+          }
+        }
+      }
+    } catch (InterruptedException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("waiting for available object interrupted", e);
+      }
+      throw new PoolInterruptedException(
+        "Interrupted while waiting for an available object",
+        e);
+    } finally {
+      this.poolLock.unlock();
+    }
+    return l;
+  }
+
+
+  /** {@inheritDoc} */
+  public void checkIn(final Ldap l)
+  {
+    final boolean valid = this.validateAndPassivate(l);
+    final PooledLdap<Ldap> pl = new PooledLdap<Ldap>(l);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting on pool lock for check in " + this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      if (this.active.remove(pl)) {
+        if (valid) {
+          this.available.add(pl);
+          if (this.logger.isTraceEnabled()) {
+            this.logger.trace("returned active ldap object: " + l);
+          }
+          this.poolNotEmpty.signal();
+        }
+      } else if (this.available.contains(pl)) {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn("returned available ldap object: " + l);
+        }
+      } else {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn("attempt to return unknown ldap object: " + l);
+        }
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/BlockingTimeoutException.java b/src/main/java/edu/vt/middleware/ldap/pool/BlockingTimeoutException.java
new file mode 100644
index 0000000..e8dd9b6
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/BlockingTimeoutException.java
@@ -0,0 +1,65 @@
+/*
+  $Id: BlockingTimeoutException.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+/**
+ * <code>BlockingTimeoutException</code> is thrown when a blocking operation
+ * times out. See {@link BlockingLdapPool#checkOut()}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class BlockingTimeoutException extends LdapPoolException
+{
+
+  /** serialVersionUID. */
+  private static final long serialVersionUID = -5152940431346111294L;
+
+
+  /**
+   * This creates a new <code>BlockingTimeoutException</code> with the supplied
+   * <code>String</code>.
+   *
+   * @param  msg  <code>String</code>
+   */
+  public BlockingTimeoutException(final String msg)
+  {
+    super(msg);
+  }
+
+
+  /**
+   * This creates a new <code>BlockingTimeoutException</code> with the supplied
+   * <code>Exception</code>.
+   *
+   * @param  e  <code>Exception</code>
+   */
+  public BlockingTimeoutException(final Exception e)
+  {
+    super(e);
+  }
+
+
+  /**
+   * This creates a new <code>BlockingTimeoutException</code> with the supplied
+   * <code>String</code> and <code>Exception</code>.
+   *
+   * @param  msg  <code>String</code>
+   * @param  e  <code>Exception</code>
+   */
+  public BlockingTimeoutException(final String msg, final Exception e)
+  {
+    super(msg, e);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/CloseLdapPassivator.java b/src/main/java/edu/vt/middleware/ldap/pool/CloseLdapPassivator.java
new file mode 100644
index 0000000..a1dcaa5
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/CloseLdapPassivator.java
@@ -0,0 +1,44 @@
+/*
+  $Id: CloseLdapPassivator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import edu.vt.middleware.ldap.Ldap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>CloseLdapPassivator</code> passivates an ldap object by attempting to
+ * close it's connection to the ldap.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class CloseLdapPassivator implements LdapPassivator<Ldap>
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+
+  /** {@inheritDoc} */
+  public boolean passivate(final Ldap l)
+  {
+    boolean success = false;
+    if (l != null) {
+      l.close();
+      success = true;
+    }
+    return success;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/CompareLdapValidator.java b/src/main/java/edu/vt/middleware/ldap/pool/CompareLdapValidator.java
new file mode 100644
index 0000000..80394dd
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/CompareLdapValidator.java
@@ -0,0 +1,121 @@
+/*
+  $Id: CompareLdapValidator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>CompareLdapValidator</code> validates an ldap connection is healthy by
+ * performing a compare operation.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class CompareLdapValidator implements LdapValidator<Ldap>
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** DN for validating connections. Default value is {@value}. */
+  private String validateDn = "";
+
+  /** Filter for validating connections. Default value is {@value}. */
+  private SearchFilter validateFilter = new SearchFilter("(objectClass=*)");
+
+
+  /** Default constructor. */
+  public CompareLdapValidator() {}
+
+
+  /**
+   * Creates a new <code>CompareLdapValidator</code> with the supplied compare
+   * dn and filter.
+   *
+   * @param  dn  to use for compares
+   * @param  filter  to use for compares
+   */
+  public CompareLdapValidator(final String dn, final SearchFilter filter)
+  {
+    this.validateDn = dn;
+    this.validateFilter = filter;
+  }
+
+
+  /**
+   * Returns the validate DN.
+   *
+   * @return  validate DN
+   */
+  public String getValidateDn()
+  {
+    return this.validateDn;
+  }
+
+
+  /**
+   * Returns the validate filter.
+   *
+   * @return  validate filter
+   */
+  public SearchFilter getValidateFilter()
+  {
+    return this.validateFilter;
+  }
+
+
+  /**
+   * Sets the validate DN.
+   *
+   * @param  s  DN
+   */
+  public void setValidateDn(final String s)
+  {
+    this.validateDn = s;
+  }
+
+
+  /**
+   * Sets the validate filter.
+   *
+   * @param  filter  to compare with
+   */
+  public void setValidateFilter(final SearchFilter filter)
+  {
+    this.validateFilter = filter;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean validate(final Ldap l)
+  {
+    boolean success = false;
+    if (l != null) {
+      try {
+        success = l.compare(this.validateDn, this.validateFilter);
+      } catch (NamingException e) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug(
+            "validation failed for compare " + this.validateFilter,
+            e);
+        }
+      }
+    }
+    return success;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/ConnectLdapActivator.java b/src/main/java/edu/vt/middleware/ldap/pool/ConnectLdapActivator.java
new file mode 100644
index 0000000..5913bba
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/ConnectLdapActivator.java
@@ -0,0 +1,51 @@
+/*
+  $Id: ConnectLdapActivator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.Ldap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>ConnectLdapActivator</code> activates an ldap object by attempting to
+ * connect to the ldap.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class ConnectLdapActivator implements LdapActivator<Ldap>
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+
+  /** {@inheritDoc} */
+  public boolean activate(final Ldap l)
+  {
+    boolean success = false;
+    if (l != null) {
+      try {
+        l.connect();
+        success = true;
+      } catch (NamingException e) {
+        if (this.logger.isErrorEnabled()) {
+          this.logger.error("unabled to connect to the ldap", e);
+        }
+      }
+    }
+    return success;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/ConnectLdapValidator.java b/src/main/java/edu/vt/middleware/ldap/pool/ConnectLdapValidator.java
new file mode 100644
index 0000000..7795f40
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/ConnectLdapValidator.java
@@ -0,0 +1,50 @@
+/*
+  $Id: ConnectLdapValidator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.Ldap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>ConnectLdapValidator</code> validates an ldap connection is healthy by
+ * testing it is connected.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class ConnectLdapValidator implements LdapValidator<Ldap>
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+
+  /** {@inheritDoc} */
+  public boolean validate(final Ldap l)
+  {
+    boolean success = false;
+    if (l != null) {
+      try {
+        success = l.connect();
+      } catch (NamingException e) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("validation failed for " + l, e);
+        }
+      }
+    }
+    return success;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/DefaultLdapFactory.java b/src/main/java/edu/vt/middleware/ldap/pool/DefaultLdapFactory.java
new file mode 100644
index 0000000..73316b6
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/DefaultLdapFactory.java
@@ -0,0 +1,126 @@
+/*
+  $Id: DefaultLdapFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.io.InputStream;
+import javax.naming.NamingException;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapConfig;
+
+/**
+ * <code>DefaultLdapFactory</code> provides a simple implementation of an ldap
+ * factory.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class DefaultLdapFactory extends AbstractLdapFactory<Ldap>
+{
+
+  /** Ldap config to create ldap objects with. */
+  private LdapConfig config;
+
+  /** Whether to connect to the ldap on object creation. */
+  private boolean connectOnCreate = true;
+
+
+  /**
+   * This creates a new <code>DefaultLdapFactory</code> with the default
+   * properties file, which must be located in your classpath.
+   */
+  public DefaultLdapFactory()
+  {
+    this.config = LdapConfig.createFromProperties(null);
+    this.config.makeImmutable();
+  }
+
+
+  /**
+   * This creates a new <code>DefaultLdapFactory</code> with the supplied input
+   * stream.
+   *
+   * @param  is  <code>InputStream</code>
+   */
+  public DefaultLdapFactory(final InputStream is)
+  {
+    this.config = LdapConfig.createFromProperties(is);
+    this.config.makeImmutable();
+  }
+
+
+  /**
+   * This creates a new <code>DefaultLdapFactory</code> with the supplied ldap
+   * configuration. The ldap configuration will be marked as immutable by this
+   * factory.
+   *
+   * @param  lc  ldap config
+   */
+  public DefaultLdapFactory(final LdapConfig lc)
+  {
+    this.config = lc;
+    this.config.makeImmutable();
+  }
+
+
+  /**
+   * Returns whether ldap objects will attempt to connect after creation.
+   * Default is true.
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean getConnectOnCreate()
+  {
+    return this.connectOnCreate;
+  }
+
+
+  /**
+   * This sets whether newly created ldap objects will attempt to connect.
+   * Default is true.
+   *
+   * @param  b  connect on create
+   */
+  public void setConnectOnCreate(final boolean b)
+  {
+    this.connectOnCreate = b;
+  }
+
+
+  /** {@inheritDoc} */
+  public Ldap create()
+  {
+    Ldap l = new Ldap(this.config);
+    if (this.connectOnCreate) {
+      try {
+        l.connect();
+      } catch (NamingException e) {
+        if (this.logger.isErrorEnabled()) {
+          this.logger.error("unabled to connect to the ldap", e);
+        }
+        l = null;
+      }
+    }
+    return l;
+  }
+
+
+  /** {@inheritDoc} */
+  public void destroy(final Ldap l)
+  {
+    l.close();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("destroyed ldap object: " + l);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapActivationException.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapActivationException.java
new file mode 100644
index 0000000..258cc2f
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapActivationException.java
@@ -0,0 +1,65 @@
+/*
+  $Id: LdapActivationException.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+/**
+ * <code>LdapActivationException</code> is thrown when an attempt to activate a
+ * ldap object fails. See {@link LdapFactory#activate}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdapActivationException extends LdapPoolException
+{
+
+  /** serialVersionUID. */
+  private static final long serialVersionUID = -6185502955113178610L;
+
+
+  /**
+   * This creates a new <code>LdapActivationException</code> with the supplied
+   * <code>String</code>.
+   *
+   * @param  msg  <code>String</code>
+   */
+  public LdapActivationException(final String msg)
+  {
+    super(msg);
+  }
+
+
+  /**
+   * This creates a new <code>LdapActivationException</code> with the supplied
+   * <code>Exception</code>.
+   *
+   * @param  e  <code>Exception</code>
+   */
+  public LdapActivationException(final Exception e)
+  {
+    super(e);
+  }
+
+
+  /**
+   * This creates a new <code>LdapActivationException</code> with the supplied
+   * <code>String</code> and <code>Exception</code>.
+   *
+   * @param  msg  <code>String</code>
+   * @param  e  <code>Exception</code>
+   */
+  public LdapActivationException(final String msg, final Exception e)
+  {
+    super(msg, e);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapActivator.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapActivator.java
new file mode 100644
index 0000000..ff05aea
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapActivator.java
@@ -0,0 +1,39 @@
+/*
+  $Id: LdapActivator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import edu.vt.middleware.ldap.BaseLdap;
+
+/**
+ * <code>LdapActivator</code> provides an interface for activating ldap objects
+ * when they enter the pool.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapActivator<T extends BaseLdap>
+{
+
+
+  /**
+   * Activate the supplied ldap object.
+   *
+   * @param  t  ldap object
+   *
+   * @return  whether activation was successful
+   */
+  boolean activate(T t);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapFactory.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapFactory.java
new file mode 100644
index 0000000..3090f10
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapFactory.java
@@ -0,0 +1,75 @@
+/*
+  $Id: LdapFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import edu.vt.middleware.ldap.BaseLdap;
+
+/**
+ * <code>LdapFactory</code> provides an interface for creating, activating,
+ * validating, and destroying ldap objects.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapFactory<T extends BaseLdap>
+{
+
+
+  /**
+   * Create a new ldap object.
+   *
+   * @return  ldap object
+   */
+  T create();
+
+
+  /**
+   * Destroy an ldap object.
+   *
+   * @param  t  ldap object
+   */
+  void destroy(T t);
+
+
+  /**
+   * Prepare the supplied object for placement in the pool.
+   *
+   * @param  t  ldap object
+   *
+   * @return  whether the supplied object successfully activated
+   */
+  boolean activate(T t);
+
+
+  /**
+   * Prepare the supplied object for removal from the pool.
+   *
+   * @param  t  ldap object
+   *
+   * @return  whether the supplied object successfully passivated
+   */
+  boolean passivate(T t);
+
+
+  /**
+   * Verify an ldap object is still viable for use in the pool.
+   *
+   * @param  t  ldap object
+   *
+   * @return  whether the supplied object is ready for use
+   */
+  boolean validate(T t);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapPassivator.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapPassivator.java
new file mode 100644
index 0000000..7282b52
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapPassivator.java
@@ -0,0 +1,39 @@
+/*
+  $Id: LdapPassivator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import edu.vt.middleware.ldap.BaseLdap;
+
+/**
+ * <code>LdapPasivator</code> provides an interface for passivating ldap objects
+ * when they are checked back into the pool.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapPassivator<T extends BaseLdap>
+{
+
+
+  /**
+   * Passivate the supplied ldap object.
+   *
+   * @param  t  ldap object
+   *
+   * @return  whether passivation was successful
+   */
+  boolean passivate(T t);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapPool.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapPool.java
new file mode 100644
index 0000000..eb362b3
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapPool.java
@@ -0,0 +1,107 @@
+/*
+  $Id: LdapPool.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.util.Timer;
+import edu.vt.middleware.ldap.BaseLdap;
+
+/**
+ * <code>LdapPool</code> provides an interface for pooling ldap objects.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapPool<T extends BaseLdap>
+{
+
+
+  /**
+   * Returns the configuration for this pool.
+   *
+   * @return  ldap pool config
+   */
+  LdapPoolConfig getLdapPoolConfig();
+
+
+  /**
+   * Sets the pool to use an existing timer. Pool will use an internal timer if
+   * none is provided. Must be called before {@link #initialize()}.
+   *
+   * @param  t  timer used to schedule pool tasks
+   */
+  void setPoolTimer(Timer t);
+
+
+  /** Initialize this pool for use. */
+  void initialize();
+
+
+  /** Empty this pool, closing all connections, and freeing any resources. */
+  void close();
+
+
+  /**
+   * Returns an ldap object from the pool.
+   *
+   * @return  ldap object
+   *
+   * @throws  LdapPoolException  if this operation fails
+   * @throws  BlockingTimeoutException  if this pool is configured with a block
+   * time and it occurs
+   * @throws  PoolInterruptedException  if this pool is configured with a block
+   * time and the current thread is interrupted
+   */
+  T checkOut()
+    throws LdapPoolException;
+
+
+  /**
+   * Returns an ldap object to the pool.
+   *
+   * @param  t  ldap object
+   */
+  void checkIn(final T t);
+
+
+  /**
+   * Attempts to reduce the size of the pool back to it's configured minimum.
+   * {@link LdapPoolConfig#setMinPoolSize(int)}.
+   */
+  void prune();
+
+
+  /**
+   * Attempts to validate all objects in the pool. {@link
+   * LdapPoolConfig#setValidatePeriodically(boolean)}.
+   */
+  void validate();
+
+
+  /**
+   * Returns the number of ldap objects available for use.
+   *
+   * @return  count
+   */
+  int availableCount();
+
+
+  /**
+   * Returns the number of ldap objects in use.
+   *
+   * @return  count
+   */
+  int activeCount();
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolConfig.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolConfig.java
new file mode 100644
index 0000000..7786b58
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolConfig.java
@@ -0,0 +1,379 @@
+/*
+  $Id: LdapPoolConfig.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.io.InputStream;
+import edu.vt.middleware.ldap.props.AbstractPropertyConfig;
+import edu.vt.middleware.ldap.props.LdapConfigPropertyInvoker;
+import edu.vt.middleware.ldap.props.LdapProperties;
+
+/**
+ * <code>LdapPoolConfig</code> contains all the configuration data that the
+ * pooling implementations need to control the pool.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class LdapPoolConfig extends AbstractPropertyConfig
+{
+
+  /** Domain to look for ldap properties in, value is {@value}. */
+  public static final String PROPERTIES_DOMAIN = "edu.vt.middleware.ldap.pool.";
+
+  /** Default min pool size, value is {@value}. */
+  public static final int DEFAULT_MIN_POOL_SIZE = 3;
+
+  /** Default max pool size, value is {@value}. */
+  public static final int DEFAULT_MAX_POOL_SIZE = 10;
+
+  /** Default validate on check in, value is {@value}. */
+  public static final boolean DEFAULT_VALIDATE_ON_CHECKIN = false;
+
+  /** Default validate on check out, value is {@value}. */
+  public static final boolean DEFAULT_VALIDATE_ON_CHECKOUT = false;
+
+  /** Default validate periodically, value is {@value}. */
+  public static final boolean DEFAULT_VALIDATE_PERIODICALLY = false;
+
+  /** Default validate timer period, value is {@value}. */
+  public static final long DEFAULT_VALIDATE_TIMER_PERIOD = 1800000;
+
+  /** Default prune timer period, value is {@value}. */
+  public static final long DEFAULT_PRUNE_TIMER_PERIOD = 300000;
+
+  /** Default expiration time, value is {@value}. */
+  public static final long DEFAULT_EXPIRATION_TIME = 600000;
+
+  /** Invoker for ldap properties. */
+  private static final LdapConfigPropertyInvoker PROPERTIES =
+    new LdapConfigPropertyInvoker(LdapPoolConfig.class, PROPERTIES_DOMAIN);
+
+  /** Min pool size. */
+  private int minPoolSize = DEFAULT_MIN_POOL_SIZE;
+
+  /** Max pool size. */
+  private int maxPoolSize = DEFAULT_MAX_POOL_SIZE;
+
+  /** Whether the ldap object should be validated when returned to the pool. */
+  private boolean validateOnCheckIn = DEFAULT_VALIDATE_ON_CHECKIN;
+
+  /** Whether the ldap object should be validated when given from the pool. */
+  private boolean validateOnCheckOut = DEFAULT_VALIDATE_ON_CHECKOUT;
+
+  /** Whether the pool should be validated periodically. */
+  private boolean validatePeriodically = DEFAULT_VALIDATE_PERIODICALLY;
+
+  /** Time in milliseconds that the validate pool timer should repeat. */
+  private long validateTimerPeriod = DEFAULT_VALIDATE_TIMER_PERIOD;
+
+  /** Time in milliseconds that the prune pool timer should repeat. */
+  private long pruneTimerPeriod = DEFAULT_PRUNE_TIMER_PERIOD;
+
+  /** Time in milliseconds that ldap objects should be considered expired. */
+  private long expirationTime = DEFAULT_EXPIRATION_TIME;
+
+
+  /** Default constructor. */
+  public LdapPoolConfig() {}
+
+
+  /**
+   * This returns the min pool size for the <code>LdapPoolConfig</code>. Default
+   * value is {@link #DEFAULT_MIN_POOL_SIZE}. This value represents the size of
+   * the pool after the prune timer has run.
+   *
+   * @return  <code>int</code> - min pool size
+   */
+  public int getMinPoolSize()
+  {
+    return this.minPoolSize;
+  }
+
+
+  /**
+   * This returns the max pool size for the <code>LdapPoolConfig</code>. Default
+   * value is {@link #DEFAULT_MAX_POOL_SIZE}. This value may or may not be
+   * strictly enforced depending on the pooling implementation.
+   *
+   * @return  <code>int</code> - max pool size
+   */
+  public int getMaxPoolSize()
+  {
+    return this.maxPoolSize;
+  }
+
+
+  /**
+   * This returns the validate on check in flag for the <code>
+   * LdapPoolConfig</code>. Default value is {@link
+   * #DEFAULT_VALIDATE_ON_CHECKIN}.
+   *
+   * @return  <code>boolean</code> - validate on check in
+   */
+  public boolean isValidateOnCheckIn()
+  {
+    return this.validateOnCheckIn;
+  }
+
+
+  /**
+   * This returns the validate on check out flag for the <code>
+   * LdapPoolConfig</code>. Default value is {@link
+   * #DEFAULT_VALIDATE_ON_CHECKOUT}.
+   *
+   * @return  <code>boolean</code> - validate on check in
+   */
+  public boolean isValidateOnCheckOut()
+  {
+    return this.validateOnCheckOut;
+  }
+
+
+  /**
+   * This returns the validate periodically flag for the <code>
+   * LdapPoolConfig</code>. Default value is {@link
+   * #DEFAULT_VALIDATE_PERIODICALLY}.
+   *
+   * @return  <code>boolean</code> - validate periodically
+   */
+  public boolean isValidatePeriodically()
+  {
+    return this.validatePeriodically;
+  }
+
+
+  /**
+   * This returns the prune timer period for the <code>LdapPoolConfig</code>.
+   * Default value is {@link #DEFAULT_PRUNE_TIMER_PERIOD}. The prune timer
+   * attempts to execute {@link LdapPool#prune()}.
+   *
+   * @return  <code>long</code> - prune timer period in milliseconds
+   */
+  public long getPruneTimerPeriod()
+  {
+    return this.pruneTimerPeriod;
+  }
+
+
+  /**
+   * This returns the validate timer period for the <code>LdapPoolConfig</code>.
+   * Default value is {@link #DEFAULT_VALIDATE_TIMER_PERIOD}. The validate timer
+   * attempts to execute {@link LdapPool#validate()}.
+   *
+   * @return  <code>long</code> - validate timer period in milliseconds
+   */
+  public long getValidateTimerPeriod()
+  {
+    return this.validateTimerPeriod;
+  }
+
+
+  /**
+   * This returns the expiration time for the <code>LdapPoolConfig</code>.
+   * Default value is {@link #DEFAULT_EXPIRATION_TIME}. The expiration time
+   * represents the max time an ldap object should be available before it is
+   * considered stale. This value does not apply to objects in the pool if the
+   * pool has only a minimum number of objects available.
+   *
+   * @return  <code>long</code> - expiration time in milliseconds
+   */
+  public long getExpirationTime()
+  {
+    return this.expirationTime;
+  }
+
+
+  /**
+   * This sets the min pool size for the <code>LdapPoolConfig</code>.
+   *
+   * @param  size  <code>int</code>
+   */
+  public void setMinPoolSize(final int size)
+  {
+    checkImmutable();
+    if (size >= 0) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("setting minPoolSize: " + size);
+      }
+      this.minPoolSize = size;
+    }
+  }
+
+
+  /**
+   * This sets the max pool size for the <code>LdapPoolConfig</code>.
+   *
+   * @param  size  <code>int</code>
+   */
+  public void setMaxPoolSize(final int size)
+  {
+    checkImmutable();
+    if (size >= 0) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("setting maxPoolSize: " + size);
+      }
+      this.maxPoolSize = size;
+    }
+  }
+
+
+  /**
+   * This sets the validate on check in flag for the <code>
+   * LdapPoolConfig</code>.
+   *
+   * @param  b  <code>boolean</code>
+   */
+  public void setValidateOnCheckIn(final boolean b)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting validateOnCheckIn: " + b);
+    }
+    this.validateOnCheckIn = b;
+  }
+
+
+  /**
+   * This sets the validate on check out flag for the <code>
+   * LdapPoolConfig</code>.
+   *
+   * @param  b  <code>boolean</code>
+   */
+  public void setValidateOnCheckOut(final boolean b)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting validateOnCheckOut: " + b);
+    }
+    this.validateOnCheckOut = b;
+  }
+
+
+  /**
+   * This sets the validate periodically flag for the <code>
+   * LdapPoolConfig</code>.
+   *
+   * @param  b  <code>boolean</code>
+   */
+  public void setValidatePeriodically(final boolean b)
+  {
+    checkImmutable();
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace("setting validatePeriodically: " + b);
+    }
+    this.validatePeriodically = b;
+  }
+
+
+  /**
+   * Sets the period for which the prune pool timer will run.
+   *
+   * @param  time  in milliseconds
+   */
+  public void setPruneTimerPeriod(final long time)
+  {
+    checkImmutable();
+    if (time >= 0) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("setting pruneTimerPeriod: " + time);
+      }
+      this.pruneTimerPeriod = time;
+    }
+  }
+
+
+  /**
+   * Sets the period for which the validate pool timer will run.
+   *
+   * @param  time  in milliseconds
+   */
+  public void setValidateTimerPeriod(final long time)
+  {
+    checkImmutable();
+    if (time >= 0) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("setting validateTimerPeriod: " + time);
+      }
+      this.validateTimerPeriod = time;
+    }
+  }
+
+
+  /**
+   * Sets the time that an ldap object should be considered stale and ready for
+   * removal from the pool.
+   *
+   * @param  time  in milliseconds
+   */
+  public void setExpirationTime(final long time)
+  {
+    checkImmutable();
+    if (time >= 0) {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("setting expirationTime: " + time);
+      }
+      this.expirationTime = time;
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public String getPropertiesDomain()
+  {
+    return PROPERTIES_DOMAIN;
+  }
+
+
+  /** {@inheritDoc} */
+  public void setEnvironmentProperties(final String name, final String value)
+  {
+    checkImmutable();
+    if (name != null && value != null) {
+      if (PROPERTIES.hasProperty(name)) {
+        PROPERTIES.setProperty(this, name, value);
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean hasEnvironmentProperty(final String name)
+  {
+    return PROPERTIES.hasProperty(name);
+  }
+
+
+  /**
+   * Create an instance of this class initialized with properties from the input
+   * stream. If the input stream is null, load properties from the default
+   * properties file.
+   *
+   * @param  is  to load properties from
+   *
+   * @return  <code>LdapPoolConfig</code> initialized ldap pool config
+   */
+  public static LdapPoolConfig createFromProperties(final InputStream is)
+  {
+    final LdapPoolConfig poolConfig = new LdapPoolConfig();
+    LdapProperties properties = null;
+    if (is != null) {
+      properties = new LdapProperties(poolConfig, is);
+    } else {
+      properties = new LdapProperties(poolConfig);
+      properties.useDefaultPropertiesFile();
+    }
+    properties.configure();
+    return poolConfig;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolException.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolException.java
new file mode 100644
index 0000000..37c9009
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolException.java
@@ -0,0 +1,65 @@
+/*
+  $Id: LdapPoolException.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+/**
+ * <code>LdapPoolException</code> is the base exception thrown when a pool
+ * operation fails.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdapPoolException extends Exception
+{
+
+  /** serialVersionUID. */
+  private static final long serialVersionUID = 4077412841480524865L;
+
+
+  /**
+   * This creates a new <code>LdapPoolException</code> with the supplied <code>
+   * String</code>.
+   *
+   * @param  msg  <code>String</code>
+   */
+  public LdapPoolException(final String msg)
+  {
+    super(msg);
+  }
+
+
+  /**
+   * This creates a new <code>LdapPoolException</code> with the supplied <code>
+   * Exception</code>.
+   *
+   * @param  e  <code>Exception</code>
+   */
+  public LdapPoolException(final Exception e)
+  {
+    super(e);
+  }
+
+
+  /**
+   * This creates a new <code>LdapPoolException</code> with the supplied <code>
+   * String</code> and <code>Exception</code>.
+   *
+   * @param  msg  <code>String</code>
+   * @param  e  <code>Exception</code>
+   */
+  public LdapPoolException(final String msg, final Exception e)
+  {
+    super(msg, e);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolExhaustedException.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolExhaustedException.java
new file mode 100644
index 0000000..d77dd58
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapPoolExhaustedException.java
@@ -0,0 +1,65 @@
+/*
+  $Id: LdapPoolExhaustedException.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+/**
+ * <code>LdapPoolExhaustedException</code> is thrown when the pool is empty and
+ * no need requests can be serviced.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdapPoolExhaustedException extends LdapPoolException
+{
+
+  /** serialVersionUID. */
+  private static final long serialVersionUID = 900885030182519501L;
+
+
+  /**
+   * This creates a new <code>LdapPoolExhaustedException</code> with the
+   * supplied <code>String</code>.
+   *
+   * @param  msg  <code>String</code>
+   */
+  public LdapPoolExhaustedException(final String msg)
+  {
+    super(msg);
+  }
+
+
+  /**
+   * This creates a new <code>LdapPoolExhaustedException</code> with the
+   * supplied <code>Exception</code>.
+   *
+   * @param  e  <code>Exception</code>
+   */
+  public LdapPoolExhaustedException(final Exception e)
+  {
+    super(e);
+  }
+
+
+  /**
+   * This creates a new <code>LdapPoolExhaustedException</code> with the
+   * supplied <code>String</code> and <code>Exception</code>.
+   *
+   * @param  msg  <code>String</code>
+   * @param  e  <code>Exception</code>
+   */
+  public LdapPoolExhaustedException(final String msg, final Exception e)
+  {
+    super(msg, e);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapValidationException.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapValidationException.java
new file mode 100644
index 0000000..63b81fe
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapValidationException.java
@@ -0,0 +1,65 @@
+/*
+  $Id: LdapValidationException.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+/**
+ * <code>LdapValidationException</code> is thrown when an attempt to validate a
+ * ldap object fails. See {@link LdapFactory#validate}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdapValidationException extends LdapPoolException
+{
+
+  /** serialVersionUID. */
+  private static final long serialVersionUID = -3130116579807362686L;
+
+
+  /**
+   * This creates a new <code>LdapValidationException</code> with the supplied
+   * <code>String</code>.
+   *
+   * @param  msg  <code>String</code>
+   */
+  public LdapValidationException(final String msg)
+  {
+    super(msg);
+  }
+
+
+  /**
+   * This creates a new <code>LdapValidationException</code> with the supplied
+   * <code>Exception</code>.
+   *
+   * @param  e  <code>Exception</code>
+   */
+  public LdapValidationException(final Exception e)
+  {
+    super(e);
+  }
+
+
+  /**
+   * This creates a new <code>LdapValidationException</code> with the supplied
+   * <code>String</code> and <code>Exception</code>.
+   *
+   * @param  msg  <code>String</code>
+   * @param  e  <code>Exception</code>
+   */
+  public LdapValidationException(final String msg, final Exception e)
+  {
+    super(msg, e);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/LdapValidator.java b/src/main/java/edu/vt/middleware/ldap/pool/LdapValidator.java
new file mode 100644
index 0000000..43ac300
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/LdapValidator.java
@@ -0,0 +1,39 @@
+/*
+  $Id: LdapValidator.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import edu.vt.middleware.ldap.BaseLdap;
+
+/**
+ * <code>LdapValidator</code> provides an interface for validating ldap objects
+ * when they are in the pool.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface LdapValidator<T extends BaseLdap>
+{
+
+
+  /**
+   * Validate the supplied ldap object.
+   *
+   * @param  t  ldap object
+   *
+   * @return  whether validation was successful
+   */
+  boolean validate(T t);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/PoolInterruptedException.java b/src/main/java/edu/vt/middleware/ldap/pool/PoolInterruptedException.java
new file mode 100644
index 0000000..ff4c4e8
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/PoolInterruptedException.java
@@ -0,0 +1,65 @@
+/*
+  $Id: PoolInterruptedException.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+/**
+ * <code>PoolInterruptedException</code> is thrown when a pool thread is
+ * unexpectedly interrupted while blocking.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class PoolInterruptedException extends LdapPoolException
+{
+
+  /** serialVersionUID. */
+  private static final long serialVersionUID = 3788775913431470860L;
+
+
+  /**
+   * This creates a new <code>PoolInterruptedException</code> with the supplied
+   * <code>String</code>.
+   *
+   * @param  msg  <code>String</code>
+   */
+  public PoolInterruptedException(final String msg)
+  {
+    super(msg);
+  }
+
+
+  /**
+   * This creates a new <code>PoolInterruptedException</code> with the supplied
+   * <code>Exception</code>.
+   *
+   * @param  e  <code>Exception</code>
+   */
+  public PoolInterruptedException(final Exception e)
+  {
+    super(e);
+  }
+
+
+  /**
+   * This creates a new <code>PoolInterruptedException</code> with the supplied
+   * <code>String</code> and <code>Exception</code>.
+   *
+   * @param  msg  <code>String</code>
+   * @param  e  <code>Exception</code>
+   */
+  public PoolInterruptedException(final String msg, final Exception e)
+  {
+    super(msg, e);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/PrunePoolTask.java b/src/main/java/edu/vt/middleware/ldap/pool/PrunePoolTask.java
new file mode 100644
index 0000000..cd442b6
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/PrunePoolTask.java
@@ -0,0 +1,73 @@
+/*
+  $Id: PrunePoolTask.java 2790 2013-07-11 17:50:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2790 $
+  Updated: $Date: 2013-07-11 18:50:53 +0100 (Thu, 11 Jul 2013) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.util.TimerTask;
+import edu.vt.middleware.ldap.BaseLdap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>PrunePoolTask</code> is a periodic task that removes available ldap
+ * objects from the pool if the objects have been in the pool longer than a
+ * configured expiration time and the pool size is above it's configured
+ * minimum. Task will skip execution if the pool has any active objects.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2790 $ $Date: 2013-07-11 18:50:53 +0100 (Thu, 11 Jul 2013) $
+ */
+public class PrunePoolTask<T extends BaseLdap> extends TimerTask
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Pool to clean. */
+  private LdapPool<T> pool;
+
+
+  /**
+   * Creates a new task to periodically prune the supplied pool.
+   *
+   * @param  lp  ldap pool to periodically inspect
+   */
+  public PrunePoolTask(final LdapPool<T> lp)
+  {
+    this.pool = lp;
+  }
+
+
+  /**
+   * This attempts to remove idle objects from a pool. See {@link
+   * LdapPool#prune()}.
+   */
+  public void run()
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Begin prune task for " + this.pool);
+    }
+    try {
+      this.pool.prune();
+    } catch (Exception e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Prune task failed for " + this.pool, e);
+      }
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("End prune task for " + this.pool);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/SharedLdapPool.java b/src/main/java/edu/vt/middleware/ldap/pool/SharedLdapPool.java
new file mode 100644
index 0000000..cfe3076
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/SharedLdapPool.java
@@ -0,0 +1,224 @@
+/*
+  $Id: SharedLdapPool.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.util.NoSuchElementException;
+import edu.vt.middleware.ldap.Ldap;
+
+/**
+ * <code>SharedLdapPool</code> implements a pool of ldap objects that has a set
+ * minimum and maximum size. The pool will not grow beyond the maximum size and
+ * when the pool is exhausted, requests for new objects will be serviced by
+ * objects that are already in use. Since {@link edu.vt.middleware.ldap.Ldap} is
+ * a thread safe object this implementation leverages that by sharing ldap
+ * objects among requests. See {@link
+ * javax.naming.ldap.LdapContext#newInstance(Control[])}. This implementation
+ * should be used when you want some control over the maximum number of ldap
+ * connections, but can tolerate some new connections under high load. See
+ * {@link AbstractLdapPool}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class SharedLdapPool extends AbstractLdapPool<Ldap>
+{
+
+
+  /** Creates a new ldap pool using {@link DefaultLdapFactory}. */
+  public SharedLdapPool()
+  {
+    super(new LdapPoolConfig(), new DefaultLdapFactory());
+  }
+
+
+  /**
+   * Creates a new ldap pool with the supplied ldap factory.
+   *
+   * @param  lf  ldap factory
+   */
+  public SharedLdapPool(final LdapFactory<Ldap> lf)
+  {
+    super(new LdapPoolConfig(), lf);
+  }
+
+
+  /**
+   * Creates a new ldap pool with the supplied ldap config and factory.
+   *
+   * @param  lpc  ldap pool configuration
+   * @param  lf  ldap factory
+   */
+  public SharedLdapPool(final LdapPoolConfig lpc, final LdapFactory<Ldap> lf)
+  {
+    super(lpc, lf);
+  }
+
+
+  /** {@inheritDoc} */
+  public Ldap checkOut()
+    throws LdapPoolException
+  {
+    Ldap l = null;
+    boolean create = false;
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting on pool lock for check out " + this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      // if an available object exists, use it
+      // if no available objects and the pool can grow, attempt to create
+      // otherwise the pool is full, return a shared object
+      if (this.active.size() < this.available.size()) {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("retrieve available ldap object");
+        }
+        l = this.retrieveAvailable();
+      } else if (this.active.size() < this.poolConfig.getMaxPoolSize()) {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("pool can grow, attempt to create ldap object");
+        }
+        create = true;
+      } else {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace(
+            "pool is full, " +
+            "attempt to retrieve available ldap object");
+        }
+        l = this.retrieveAvailable();
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+
+    if (create) {
+      // previous block determined a creation should occur
+      // block here until create occurs without locking the whole pool
+      // if the pool is already maxed or creates are failing,
+      // return a shared object
+      this.checkOutLock.lock();
+      try {
+        boolean b = true;
+        this.poolLock.lock();
+        try {
+          if (this.available.size() == this.poolConfig.getMaxPoolSize()) {
+            b = false;
+          }
+        } finally {
+          this.poolLock.unlock();
+        }
+        if (b) {
+          l = this.createAvailableAndActive();
+          if (this.logger.isTraceEnabled()) {
+            this.logger.trace(
+              "created new available and active ldap object: " + l);
+          }
+        }
+      } finally {
+        this.checkOutLock.unlock();
+      }
+      if (l == null) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("create failed, retrieve available ldap object");
+        }
+        l = this.retrieveAvailable();
+      }
+    }
+
+    if (l != null) {
+      this.activateAndValidate(l);
+    } else {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Could not service check out request");
+      }
+      throw new LdapPoolExhaustedException(
+        "Pool is empty and object creation failed");
+    }
+
+    return l;
+  }
+
+
+  /**
+   * This attempts to retrieve an ldap object from the available queue. This
+   * pooling implementation guarantees there is always an object available.
+   *
+   * @return  ldap object from the pool
+   *
+   * @throws  IllegalStateException  if an object cannot be removed from the
+   * available queue
+   */
+  protected Ldap retrieveAvailable()
+  {
+    Ldap l = null;
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting on pool lock for retrieve available " +
+        this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      try {
+        final PooledLdap<Ldap> pl = this.available.remove();
+        this.active.add(new PooledLdap<Ldap>(pl.getLdap()));
+        this.available.add(new PooledLdap<Ldap>(pl.getLdap()));
+        l = pl.getLdap();
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("retrieved available ldap object: " + l);
+        }
+      } catch (NoSuchElementException e) {
+        if (this.logger.isErrorEnabled()) {
+          this.logger.error("could not remove ldap object from list", e);
+        }
+        throw new IllegalStateException("Pool is empty", e);
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+    return l;
+  }
+
+
+  /** {@inheritDoc} */
+  public void checkIn(final Ldap l)
+  {
+    final boolean valid = this.validateAndPassivate(l);
+    final PooledLdap<Ldap> pl = new PooledLdap<Ldap>(l);
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting on pool lock for check in " + this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      if (this.active.remove(pl)) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("returned active ldap object: " + l);
+        }
+      } else if (this.available.contains(pl)) {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn("returned available ldap object: " + l);
+        }
+      } else {
+        if (this.logger.isWarnEnabled()) {
+          this.logger.warn("attempt to return unknown ldap object: " + l);
+        }
+      }
+      if (!valid) {
+        this.available.remove(pl);
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/SoftLimitLdapPool.java b/src/main/java/edu/vt/middleware/ldap/pool/SoftLimitLdapPool.java
new file mode 100644
index 0000000..85ec676
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/SoftLimitLdapPool.java
@@ -0,0 +1,135 @@
+/*
+  $Id: SoftLimitLdapPool.java 2241 2012-02-07 20:08:51Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2241 $
+  Updated: $Date: 2012-02-07 20:08:51 +0000 (Tue, 07 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.util.NoSuchElementException;
+import edu.vt.middleware.ldap.Ldap;
+
+/**
+ * <code>SoftLimitLdapPool</code> implements a pool of ldap objects that has a
+ * set minimum and maximum size. The pool will grow beyond it's maximum size as
+ * necessary based on it's current load. Pool size will return to it's minimum
+ * based on the configuration of the prune timer. See {@link
+ * LdapPoolConfig#setPruneTimerPeriod} and {@link
+ * LdapPoolConfig#setExpirationTime}. This implementation should be used when
+ * you have some flexibility in the number of ldap connections that can be
+ * created to handle spikes in load. See {@link AbstractLdapPool}. Note that
+ * this pool will begin blocking if it cannot create new ldap connections.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2241 $ $Date: 2012-02-07 20:08:51 +0000 (Tue, 07 Feb 2012) $
+ */
+public class SoftLimitLdapPool extends BlockingLdapPool
+{
+
+
+  /** Creates a new ldap pool using {@link DefaultLdapFactory}. */
+  public SoftLimitLdapPool()
+  {
+    super(new LdapPoolConfig(), new DefaultLdapFactory());
+  }
+
+
+  /**
+   * Creates a new ldap pool with the supplied ldap factory.
+   *
+   * @param  lf  ldap factory
+   */
+  public SoftLimitLdapPool(final LdapFactory<Ldap> lf)
+  {
+    super(new LdapPoolConfig(), lf);
+  }
+
+
+  /**
+   * Creates a new ldap pool with the supplied ldap config and factory.
+   *
+   * @param  lpc  ldap pool configuration
+   * @param  lf  ldap factory
+   */
+  public SoftLimitLdapPool(final LdapPoolConfig lpc, final LdapFactory<Ldap> lf)
+  {
+    super(lpc, lf);
+  }
+
+
+  /** {@inheritDoc} */
+  public Ldap checkOut()
+    throws LdapPoolException
+  {
+    Ldap l = null;
+    if (this.logger.isTraceEnabled()) {
+      this.logger.trace(
+        "waiting on pool lock for check out " + this.poolLock.getQueueLength());
+    }
+    this.poolLock.lock();
+    try {
+      // if an available object exists, use it
+      // if no available objects, attempt to create
+      if (this.available.size() > 0) {
+        try {
+          if (this.logger.isTraceEnabled()) {
+            this.logger.trace("retrieve available ldap object");
+          }
+          l = this.retrieveAvailable();
+        } catch (NoSuchElementException e) {
+          if (this.logger.isErrorEnabled()) {
+            this.logger.error("could not remove ldap object from list", e);
+          }
+          throw new IllegalStateException("Pool is empty", e);
+        }
+      }
+    } finally {
+      this.poolLock.unlock();
+    }
+
+    if (l == null) {
+      // no object was available, create a new one
+      l = this.createActive();
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("created new active ldap object: " + l);
+      }
+      if (l == null) {
+        if (this.available.size() == 0 && this.active.size() == 0) {
+          if (this.logger.isErrorEnabled()) {
+            this.logger.error("Could not service check out request");
+          }
+          throw new LdapPoolExhaustedException(
+            "Pool is empty and object creation failed");
+        }
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug(
+            "create failed, block until an object is available");
+        }
+        l = this.blockAvailable();
+      } else {
+        if (this.logger.isTraceEnabled()) {
+          this.logger.trace("created new active ldap object: " + l);
+        }
+      }
+    }
+
+    if (l != null) {
+      this.activateAndValidate(l);
+    } else {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Could not service check out request");
+      }
+      throw new LdapPoolExhaustedException(
+        "Pool is empty and object creation failed");
+    }
+
+    return l;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/pool/ValidatePoolTask.java b/src/main/java/edu/vt/middleware/ldap/pool/ValidatePoolTask.java
new file mode 100644
index 0000000..7591067
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/pool/ValidatePoolTask.java
@@ -0,0 +1,71 @@
+/*
+  $Id: ValidatePoolTask.java 2790 2013-07-11 17:50:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2790 $
+  Updated: $Date: 2013-07-11 18:50:53 +0100 (Thu, 11 Jul 2013) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.util.TimerTask;
+import edu.vt.middleware.ldap.BaseLdap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>ValidatePoolTask</code> is a periodic task that checks that every ldap
+ * object in the pool is valid. Objects that don't pass validation are removed.
+ *
+ * @param  <T>  type of ldap object
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2790 $ $Date: 2013-07-11 18:50:53 +0100 (Thu, 11 Jul 2013) $
+ */
+public class ValidatePoolTask<T extends BaseLdap> extends TimerTask
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Pool to clean. */
+  private LdapPool<T> pool;
+
+
+  /**
+   * Creates a new task to periodically validate the supplied pool.
+   *
+   * @param  lp  ldap pool to periodically validate
+   */
+  public ValidatePoolTask(final LdapPool<T> lp)
+  {
+    this.pool = lp;
+  }
+
+
+  /**
+   * This attempts to validate idle objects in a pool. See {@link
+   * LdapPool#validate()}.
+   */
+  public void run()
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Begin validate task for " + this.pool);
+    }
+    try {
+      this.pool.validate();
+    } catch (Exception e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Validate task failed for " + this.pool, e); 
+      }
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("End validate task for " + this.pool);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/props/AbstractPropertyConfig.java b/src/main/java/edu/vt/middleware/ldap/props/AbstractPropertyConfig.java
new file mode 100644
index 0000000..75e6c84
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/props/AbstractPropertyConfig.java
@@ -0,0 +1,143 @@
+/*
+  $Id: AbstractPropertyConfig.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.props;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractPropertyConfig</code> provides a base implementation of <code>
+ * PropertyConfig</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class AbstractPropertyConfig implements PropertyConfig
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Whether this config has been marked immutable. */
+  private boolean immutable;
+
+
+  /** Make this property config immutable. */
+  public void makeImmutable()
+  {
+    this.immutable = true;
+  }
+
+
+  /**
+   * Verifies if this property config is immutable.
+   *
+   * @throws  IllegalStateException  if this property config is immutable
+   */
+  public void checkImmutable()
+  {
+    if (this.immutable) {
+      throw new IllegalStateException("Cannot modify immutable object");
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract String getPropertiesDomain();
+
+
+  /** {@inheritDoc} */
+  public abstract void setEnvironmentProperties(
+    final String name,
+    final String value);
+
+
+  /** {@inheritDoc} */
+  public void setEnvironmentProperties(final Properties properties)
+  {
+    if (properties != null) {
+      final Map<String, String> props = new HashMap<String, String>();
+      final Enumeration<?> en = properties.keys();
+      if (en != null) {
+        while (en.hasMoreElements()) {
+          final String name = (String) en.nextElement();
+          final String value = (String) properties.get(name);
+          if (this.hasEnvironmentProperty(name)) {
+            props.put(name, value);
+          } else {
+            this.setEnvironmentProperties(name, value);
+          }
+        }
+        for (Map.Entry<String, String> e : props.entrySet()) {
+          this.setEnvironmentProperties(e.getKey(), e.getValue());
+        }
+      }
+    }
+  }
+
+
+  /**
+   * See {@link #setEnvironmentProperties(String,String)}.
+   *
+   * @param  properties  <code>Hashtable</code> of environment properties
+   */
+  public void setEnvironmentProperties(
+    final Hashtable<String, String> properties)
+  {
+    if (properties != null) {
+      final Map<String, String> props = new HashMap<String, String>();
+      for (Map.Entry<String, String> e : properties.entrySet()) {
+        if (this.hasEnvironmentProperty(e.getKey())) {
+          props.put(e.getKey(), e.getValue());
+        } else {
+          this.setEnvironmentProperties(e.getKey(), e.getValue());
+        }
+      }
+      for (Map.Entry<String, String> e : props.entrySet()) {
+        this.setEnvironmentProperties(e.getKey(), e.getValue());
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract boolean hasEnvironmentProperty(final String name);
+
+
+  /**
+   * Verifies that a string is not null or empty.
+   *
+   * @param  s  to verify
+   * @param  allowNull  whether null strings are valid
+   *
+   * @throws  IllegalArgumentException  if the string is null or empty
+   */
+  protected void checkStringInput(final String s, final boolean allowNull)
+  {
+    if (allowNull) {
+      if (s != null && "".equals(s)) {
+        throw new IllegalArgumentException("Input cannot be empty");
+      }
+    } else {
+      if (s == null || "".equals(s)) {
+        throw new IllegalArgumentException("Input cannot be null or empty");
+      }
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/props/AbstractPropertyInvoker.java b/src/main/java/edu/vt/middleware/ldap/props/AbstractPropertyInvoker.java
new file mode 100644
index 0000000..4c38b83
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/props/AbstractPropertyInvoker.java
@@ -0,0 +1,264 @@
+/*
+  $Id: AbstractPropertyInvoker.java 1616 2010-09-21 17:22:27Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1616 $
+  Updated: $Date: 2010-09-21 18:22:27 +0100 (Tue, 21 Sep 2010) $
+*/
+package edu.vt.middleware.ldap.props;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AbstractPropertyInvoker</code> provides methods common to property
+ * invokers.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1616 $ $Date: 2010-09-21 18:22:27 +0100 (Tue, 21 Sep 2010) $
+ */
+public abstract class AbstractPropertyInvoker
+{
+
+  /** Cache of properties. */
+  protected static final Map<String, Map<String, Method[]>> PROPERTIES_CACHE =
+    new HashMap<String, Map<String, Method[]>>();
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Class to invoke methods on. */
+  protected Class<?> clazz;
+
+  /** Map of all properties to their getter and setter methods. */
+  protected Map<String, Method[]> properties;
+
+
+  /**
+   * Initializes the properties map with the supplied class.
+   *
+   * @param  c  to read methods from
+   * @param  domain  optional domain that properties are in
+   */
+  protected void initialize(final Class<?> c, final String domain)
+  {
+    final String cacheKey = new StringBuilder(c.getName()).append("@").append(
+      domain).toString();
+    if (PROPERTIES_CACHE.containsKey(cacheKey)) {
+      this.properties = PROPERTIES_CACHE.get(cacheKey);
+    } else {
+      this.properties = new HashMap<String, Method[]>();
+      PROPERTIES_CACHE.put(cacheKey, this.properties);
+      for (Method method : c.getMethods()) {
+        if (
+          method.getName().startsWith("set") &&
+            method.getParameterTypes().length == 1) {
+          final String mName = method.getName().substring(3);
+          final String pName = new StringBuilder(domain).append(
+            mName.substring(0, 1).toLowerCase()).append(
+              mName.substring(1, mName.length())).toString();
+          if (this.properties.containsKey(pName)) {
+            final Method[] m = this.properties.get(pName);
+            m[1] = method;
+            this.properties.put(pName, m);
+          } else {
+            this.properties.put(pName, new Method[] {null, method});
+          }
+        } else if (
+          method.getName().startsWith("get") &&
+            method.getParameterTypes().length == 0) {
+          final String mName = method.getName().substring(3);
+          final String pName = new StringBuilder(domain).append(
+            mName.substring(0, 1).toLowerCase()).append(
+              mName.substring(1, mName.length())).toString();
+          if (this.properties.containsKey(pName)) {
+            final Method[] m = this.properties.get(pName);
+            m[0] = method;
+            this.properties.put(pName, m);
+          } else {
+            this.properties.put(pName, new Method[] {method, null});
+          }
+        } else if (
+          "initialize".equals(method.getName()) &&
+            method.getParameterTypes().length == 0) {
+          final String pName = new StringBuilder(domain).append(
+            method.getName()).toString();
+          this.properties.put(pName, new Method[] {method, method});
+        }
+      }
+    }
+    this.clazz = c;
+  }
+
+
+  /**
+   * This invokes the setter method for the supplied property name with the
+   * supplied value. If name or value is null, then this method does nothing.
+   *
+   * @param  object  <code>Object</code> to invoke method on
+   * @param  name  <code>String</code> property name
+   * @param  value  <code>String</code> property value
+   *
+   * @throws  IllegalArgumentException  if an invocation exception occurs
+   */
+  public void setProperty(
+    final Object object,
+    final String name,
+    final String value)
+  {
+    if (!this.clazz.isInstance(object)) {
+      throw new IllegalArgumentException(
+        "Illegal attempt to set property for class " + this.clazz.getName() +
+        " on object of type " + object.getClass().getName());
+    }
+
+    final Method getter = this.properties.get(name) != null
+      ? this.properties.get(name)[0] : null;
+    if (getter == null) {
+      throw new IllegalArgumentException(
+        "No getter method found for " + name + " on object " +
+        this.clazz.getName());
+    }
+
+    final Method setter = this.properties.get(name) != null
+      ? this.properties.get(name)[1] : null;
+    if (setter == null) {
+      throw new IllegalArgumentException(
+        "No setter method found for " + name + " on object " +
+        this.clazz.getName());
+    }
+
+    invokeMethod(
+      setter,
+      object,
+      this.convertValue(getter.getReturnType(), value));
+  }
+
+
+  /**
+   * This converts the supplied string value into an Object of the appropriate
+   * supplied type. If value cannot be converted it is returned as is.
+   *
+   * @param  type  of object to convert value into
+   * @param  value  to parse
+   *
+   * @return  object of the supplied type
+   */
+  protected abstract Object convertValue(
+    final Class<?> type,
+    final String value);
+
+
+  /**
+   * This returns whether the supplied property exists.
+   *
+   * @param  name  <code>String</code> to check
+   *
+   * @return  <code>boolean</code> whether the supplied property exists
+   */
+  public boolean hasProperty(final String name)
+  {
+    return this.properties.containsKey(name);
+  }
+
+
+  /**
+   * This returns the property keys.
+   *
+   * @return  <code>Set</code> of property names
+   */
+  public Set<String> getProperties()
+  {
+    return Collections.unmodifiableSet(this.properties.keySet());
+  }
+
+
+  /**
+   * Creates an instance of the supplied type.
+   *
+   * @param  <T>  type of class returned
+   * @param  type  of class to create
+   * @param  className  to create
+   *
+   * @return  class of type T
+   *
+   * @throws  IllegalArgumentException  if the supplied class name cannot create
+   * a new instance of T
+   */
+  @SuppressWarnings("unchecked")
+  public static <T> T instantiateType(final T type, final String className)
+  {
+    try {
+      return (T) createClass(className).newInstance();
+    } catch (InstantiationException e) {
+      throw new IllegalArgumentException(e);
+    } catch (IllegalAccessException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+
+  /**
+   * Creates the class with the supplied name.
+   *
+   * @param  className  to create
+   *
+   * @return  class
+   *
+   * @throws  IllegalArgumentException  if the supplied class name cannot be
+   * created
+   */
+  public static Class<?> createClass(final String className)
+  {
+    try {
+      return Class.forName(className);
+    } catch (ClassNotFoundException e) {
+      throw new IllegalArgumentException(
+        "Could not find class '" + className + "'",
+        e);
+    }
+  }
+
+
+  /**
+   * Invokes the supplied method on the supplied object with the supplied
+   * argument.
+   *
+   * @param  method  <code>Method</code> to invoke
+   * @param  object  <code>Object</code> to invoke method on
+   * @param  arg  <code>Object</code> to invoke method with
+   *
+   * @return  <code>Object</code> produced by the invocation
+   *
+   * @throws  IllegalArgumentException  if an error occurs invoking the method
+   */
+  public static Object invokeMethod(
+    final Method method,
+    final Object object,
+    final Object arg)
+  {
+    try {
+      Object[] params = new Object[] {arg};
+      if (arg == null && method.getParameterTypes().length == 0) {
+        params = (Object[]) null;
+      }
+      return method.invoke(object, params);
+    } catch (InvocationTargetException e) {
+      throw new IllegalArgumentException(e);
+    } catch (IllegalAccessException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/props/ConfigParser.java b/src/main/java/edu/vt/middleware/ldap/props/ConfigParser.java
new file mode 100644
index 0000000..cba74c2
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/props/ConfigParser.java
@@ -0,0 +1,143 @@
+/*
+  $Id: ConfigParser.java 1501 2010-08-18 18:48:01Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1501 $
+  Updated: $Date: 2010-08-18 19:48:01 +0100 (Wed, 18 Aug 2010) $
+*/
+package edu.vt.middleware.ldap.props;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses the configuration data associated with classes that contain setter
+ * properties. The format of the property string should be like:
+ *
+ * <pre>
+   MyClass{{propertyOne=foo}{propertyTwo=bar}}
+ * </pre>
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1501 $ $Date: 2010-08-18 19:48:01 +0100 (Wed, 18 Aug 2010) $
+ */
+public class ConfigParser
+{
+
+  /** Property string containing configuration. */
+  private static final Pattern CONFIG_PATTERN = Pattern.compile(
+    "([^\\{]+)\\s*\\{(.*)\\}\\s*");
+
+  /** Pattern for finding properties. */
+  private static final Pattern PROPERTY_PATTERN = Pattern.compile(
+    "([^\\}\\{])+");
+
+  /** Class found in the config. */
+  private String className;
+
+  /** Properties found in the config to set on the class. */
+  private Map<String, String> properties = new HashMap<String, String>();
+
+
+  /**
+   * Creates a new <code>ConfigParser</code> with the supplied configuration
+   * string.
+   *
+   * @param  config  <code>String</code>
+   */
+  public ConfigParser(final String config)
+  {
+    final Matcher matcher = CONFIG_PATTERN.matcher(config);
+    if (matcher.matches()) {
+      this.className = matcher.group(1).trim();
+
+      final String props = matcher.group(2).trim();
+      final Matcher m = PROPERTY_PATTERN.matcher(props);
+      while (m.find()) {
+        final String input = m.group().trim();
+        if (input != null && !"".equals(input)) {
+          final String[] s = input.split("=");
+          this.properties.put(s[0].trim(), s[1].trim());
+        }
+      }
+    }
+  }
+
+
+  /**
+   * Returns the class name from the configuration.
+   *
+   * @return  <code>String</code> class name
+   */
+  public String getClassName()
+  {
+    return this.className;
+  }
+
+
+  /**
+   * Returns the properties from the configuration.
+   *
+   * @return  <code>Map</code> of property name to value
+   */
+  public Map<String, String> getProperties()
+  {
+    return this.properties;
+  }
+
+
+  /**
+   * Returns whether the supplied configuration data contains a config.
+   *
+   * @param  config  <code>String</code>
+   *
+   * @return  <code>boolean</code>
+   */
+  public static boolean isConfig(final String config)
+  {
+    return CONFIG_PATTERN.matcher(config).matches();
+  }
+
+
+  /**
+   * Initialize an instance of the class type with the properties contained in
+   * this config.
+   *
+   * @return  <code>Object</code> of the type the config parsed
+   */
+  public Object initializeType()
+  {
+    final Class<?> c = SimplePropertyInvoker.createClass(this.getClassName());
+    final Object o = SimplePropertyInvoker.instantiateType(
+      c,
+      this.getClassName());
+    this.setProperties(c, o);
+    return o;
+  }
+
+
+  /**
+   * Sets the properties on the supplied object.
+   *
+   * @param  c  <code>Class</code> type of the supplied object
+   * @param  o  <code>Object</code> to invoke properties on
+   */
+  protected void setProperties(final Class<?> c, final Object o)
+  {
+    final SimplePropertyInvoker invoker = new SimplePropertyInvoker(c);
+    for (Map.Entry<String, String> entry : this.getProperties().entrySet()) {
+      invoker.setProperty(o, entry.getKey(), entry.getValue());
+    }
+    if (invoker.getProperties().contains("initialize")) {
+      invoker.setProperty(o, "initialize", null);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/props/LdapConfigPropertyInvoker.java b/src/main/java/edu/vt/middleware/ldap/props/LdapConfigPropertyInvoker.java
new file mode 100644
index 0000000..a7284ee
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/props/LdapConfigPropertyInvoker.java
@@ -0,0 +1,239 @@
+/*
+  $Id: LdapConfigPropertyInvoker.java 1498 2010-08-18 14:21:37Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1498 $
+  Updated: $Date: 2010-08-18 15:21:37 +0100 (Wed, 18 Aug 2010) $
+*/
+package edu.vt.middleware.ldap.props;
+
+import java.lang.reflect.Array;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+import edu.vt.middleware.ldap.auth.DnResolver;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationResultHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthorizationHandler;
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+import edu.vt.middleware.ldap.handler.SearchResultHandler;
+import edu.vt.middleware.ldap.ssl.CredentialConfigParser;
+import edu.vt.middleware.ldap.ssl.SSLContextInitializer;
+
+/**
+ * <code>PropertyInvoker</code> stores setter methods for a class to make method
+ * invocation by property easier.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1498 $ $Date: 2010-08-18 15:21:37 +0100 (Wed, 18 Aug 2010) $
+ */
+public class LdapConfigPropertyInvoker extends AbstractPropertyInvoker
+{
+
+
+  /**
+   * Creates a new <code>PropertyInvoker</code> for the supplied class.
+   *
+   * @param  c  <code>Class</code> that has setter methods
+   * @param  propertiesDomain  <code>String</code> to prepend to each setter
+   * name
+   */
+  public LdapConfigPropertyInvoker(
+    final Class<?> c,
+    final String propertiesDomain)
+  {
+    this.initialize(c, propertiesDomain);
+  }
+
+
+  /** {@inheritDoc} */
+  protected Object convertValue(final Class<?> type, final String value)
+  {
+    Object newValue = value;
+    if (type != String.class) {
+      if (SSLSocketFactory.class.isAssignableFrom(type)) {
+        if ("null".equals(value)) {
+          newValue = null;
+        } else {
+          // use a credential reader to configure key/trust material
+          if (CredentialConfigParser.isCredentialConfig(value)) {
+            final CredentialConfigParser configParser =
+              new CredentialConfigParser(value);
+            newValue = instantiateType(
+              SSLSocketFactory.class,
+              configParser.getSslSocketFactoryClassName());
+
+            final Object credentialConfig = configParser.initializeType();
+            try {
+              // set the SSL context initializer using the credential config
+              invokeMethod(
+                newValue.getClass().getMethod(
+                  "setSSLContextInitializer",
+                  SSLContextInitializer.class),
+                newValue,
+                invokeMethod(
+                  credentialConfig.getClass().getMethod(
+                    "createSSLContextInitializer",
+                    new Class<?>[0]),
+                  credentialConfig,
+                  null));
+              // initialize the TLS socket factory.
+              invokeMethod(
+                newValue.getClass().getMethod("initialize", new Class<?>[0]),
+                newValue,
+                null);
+            } catch (NoSuchMethodException e) {
+              throw new IllegalArgumentException(e);
+            }
+            // use a standard config to initialize the socket factory
+          } else if (ConfigParser.isConfig(value)) {
+            final ConfigParser configParser = new ConfigParser(value);
+            newValue = configParser.initializeType();
+          } else {
+            newValue = instantiateType(SSLSocketFactory.class, value);
+          }
+        }
+      } else if (HostnameVerifier.class.isAssignableFrom(type)) {
+        newValue = this.createTypeFromPropertyValue(
+          HostnameVerifier.class,
+          value);
+      } else if (ConnectionHandler.class.isAssignableFrom(type)) {
+        newValue = this.createTypeFromPropertyValue(
+          ConnectionHandler.class,
+          value);
+      } else if (AuthenticationHandler.class.isAssignableFrom(type)) {
+        newValue = this.createTypeFromPropertyValue(
+          AuthenticationHandler.class,
+          value);
+      } else if (DnResolver.class.isAssignableFrom(type)) {
+        newValue = this.createTypeFromPropertyValue(DnResolver.class, value);
+      } else if (SearchResultHandler[].class.isAssignableFrom(type)) {
+        newValue = this.createArrayTypeFromPropertyValue(
+          SearchResultHandler.class,
+          value);
+      } else if (AuthenticationResultHandler[].class.isAssignableFrom(type)) {
+        newValue = this.createArrayTypeFromPropertyValue(
+          AuthenticationResultHandler.class,
+          value);
+      } else if (AuthorizationHandler[].class.isAssignableFrom(type)) {
+        newValue = this.createArrayTypeFromPropertyValue(
+          AuthorizationHandler.class,
+          value);
+      } else if (Class.class.isAssignableFrom(type)) {
+        newValue = this.createTypeFromPropertyValue(Class.class, value);
+      } else if (Class[].class.isAssignableFrom(type)) {
+        newValue = this.createArrayTypeFromPropertyValue(Class.class, value);
+      } else if (type.isEnum()) {
+        for (Object o : type.getEnumConstants()) {
+          final Enum<?> e = (Enum<?>) o;
+          if (e.name().equals(value)) {
+            newValue = o;
+          }
+        }
+      } else if (String[].class == type) {
+        newValue = value.split(",");
+      } else if (Object[].class == type) {
+        newValue = value.split(",");
+      } else if (float.class == type) {
+        newValue = Float.parseFloat(value);
+      } else if (int.class == type) {
+        newValue = Integer.parseInt(value);
+      } else if (long.class == type) {
+        newValue = Long.parseLong(value);
+      } else if (short.class == type) {
+        newValue = Short.parseShort(value);
+      } else if (double.class == type) {
+        newValue = Double.parseDouble(value);
+      } else if (boolean.class == type) {
+        newValue = Boolean.valueOf(value);
+      }
+    }
+    return newValue;
+  }
+
+
+  /**
+   * Returns the object which represents the supplied class given the supplied
+   * string representation.
+   *
+   * @param  c  <code>Class</code> type to instantiate
+   * @param  s  <code>String</code> to parse
+   *
+   * @return  <code>Object</code> of the supplied type or null
+   */
+  protected Object createTypeFromPropertyValue(final Class<?> c, final String s)
+  {
+    Object newObject = null;
+    if ("null".equals(s)) {
+      newObject = null;
+    } else {
+      if (ConfigParser.isConfig(s)) {
+        final ConfigParser configParser = new ConfigParser(s);
+        newObject = configParser.initializeType();
+      } else {
+        if (Class.class == c) {
+          newObject = createClass(s);
+        } else {
+          newObject = instantiateType(c, s);
+        }
+      }
+    }
+    return newObject;
+  }
+
+
+  /**
+   * Returns the object which represents an array of the supplied class given
+   * the supplied string representation.
+   *
+   * @param  c  <code>Class</code> type to instantiate
+   * @param  s  <code>String</code> to parse
+   *
+   * @return  <code>Object</code> that is an array or null
+   */
+  protected Object createArrayTypeFromPropertyValue(
+    final Class<?> c,
+    final String s)
+  {
+    Object newObject = null;
+    if ("null".equals(s)) {
+      newObject = null;
+    } else {
+      if (s.indexOf("},") != -1) {
+        final String[] classes = s.split("\\},");
+        newObject = Array.newInstance(c, classes.length);
+        for (int i = 0; i < classes.length; i++) {
+          classes[i] = classes[i] + "}";
+          if (ConfigParser.isConfig(classes[i])) {
+            final ConfigParser configParser = new ConfigParser(classes[i]);
+            Array.set(newObject, i, configParser.initializeType());
+          } else {
+            throw new IllegalArgumentException(
+              "Could not parse property string: " + classes[i]);
+          }
+        }
+      } else {
+        final String[] classes = s.split(",");
+        newObject = Array.newInstance(c, classes.length);
+        for (int i = 0; i < classes.length; i++) {
+          if (ConfigParser.isConfig(classes[i])) {
+            final ConfigParser configParser = new ConfigParser(classes[i]);
+            Array.set(newObject, i, configParser.initializeType());
+          } else {
+            if (Class.class == c) {
+              Array.set(newObject, i, createClass(classes[i]));
+            } else {
+              Array.set(newObject, i, instantiateType(c, classes[i]));
+            }
+          }
+        }
+      }
+    }
+    return newObject;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/props/LdapProperties.java b/src/main/java/edu/vt/middleware/ldap/props/LdapProperties.java
new file mode 100644
index 0000000..08019d1
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/props/LdapProperties.java
@@ -0,0 +1,188 @@
+/*
+  $Id: LdapProperties.java 1743 2010-11-19 17:00:18Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1743 $
+  Updated: $Date: 2010-11-19 17:00:18 +0000 (Fri, 19 Nov 2010) $
+*/
+package edu.vt.middleware.ldap.props;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>LdapProperties</code> attempts to load the configuration properties
+ * from a properties file in the classpath for a <code>PropertyConfig</code>
+ * object. The default properties file is '/ldap.properties'.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1743 $ $Date: 2010-11-19 17:00:18 +0000 (Fri, 19 Nov 2010) $
+ */
+public final class LdapProperties
+{
+
+  /** Default file to read properties from, value is {@value}. */
+  public static final String PROPERTIES_FILE = "/ldap.properties";
+
+  /** Log for this class. */
+  private final Log logger = LogFactory.getLog(LdapProperties.class);
+
+  /** Class with properties. */
+  private PropertyConfig propertyConfig;
+
+  /** Underlying properties. */
+  private Properties config;
+
+
+  /**
+   * This will create a new <code>LdapProperties</code> for the supplied
+   * properties config.
+   *
+   * @param  pc  object to set properties for
+   */
+  public LdapProperties(final PropertyConfig pc)
+  {
+    this.propertyConfig = pc;
+    this.config = new Properties();
+  }
+
+
+  /**
+   * This will create a new <code>LdapProperties</code> with the supplied
+   * properties properties config and input stream.
+   *
+   * @param  pc  object to set properties for
+   * @param  is  <code>InputStream</code> containing properties
+   */
+  public LdapProperties(final PropertyConfig pc, final InputStream is)
+  {
+    this.propertyConfig = pc;
+    this.useProperties(is);
+  }
+
+
+  /** This will load properties from the default properties file. */
+  public void useDefaultPropertiesFile()
+  {
+    this.useProperties(
+      LdapProperties.class.getResourceAsStream(PROPERTIES_FILE));
+  }
+
+
+  /**
+   * This will load properties from the supplied input stream.
+   *
+   * @param  is  <code>InputStream</code> containing properties
+   */
+  public void useProperties(final InputStream is)
+  {
+    if (this.config == null) {
+      this.config = loadProperties(is);
+    } else {
+      this.config.putAll(loadProperties(is));
+    }
+  }
+
+
+  /**
+   * This creates a <code>Properties</code> from the supplied input stream.
+   *
+   * @param  is  <code>InputStream</code>
+   *
+   * @return  <code>Properties</code>
+   */
+  private Properties loadProperties(final InputStream is)
+  {
+    final Properties properties = new Properties();
+    if (is != null) {
+      try {
+        properties.load(is);
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Loaded ldap properties from input stream");
+        }
+        is.close();
+      } catch (IOException e) {
+        if (this.logger.isErrorEnabled()) {
+          this.logger.error("Error using input stream", e);
+        }
+      }
+    } else {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Input stream was null, no properties loaded");
+      }
+    }
+    return properties;
+  }
+
+
+  /**
+   * This returns the name of the properties being used by this <code>
+   * LdapProperties</code>.
+   *
+   * @return  <code>Properties</code>
+   */
+  public Properties getProperties()
+  {
+    return this.config;
+  }
+
+
+  /**
+   * This sets the supplied key and value in the ldap properties. The key will
+   * be prepended with the appropriate namespace.
+   *
+   * @param  key  <code>String</code>
+   * @param  value  <code>String</code>
+   */
+  public void setProperty(final String key, final String value)
+  {
+    if (
+      this.propertyConfig.hasEnvironmentProperty(
+          this.propertyConfig.getPropertiesDomain() + key)) {
+      this.config.setProperty(
+        this.propertyConfig.getPropertiesDomain() + key,
+        value);
+    } else {
+      this.config.setProperty(key, value);
+    }
+  }
+
+
+  /**
+   * This returns whether the supplied key has already been set. The key will be
+   * prepended with the appropriate namespace.
+   *
+   * @param  key  <code>String</code>
+   *
+   * @return  <code>boolean</code>
+   */
+  public boolean isPropertySet(final String key)
+  {
+    boolean exists = false;
+    if (
+      this.propertyConfig.hasEnvironmentProperty(
+          this.propertyConfig.getPropertiesDomain() + key)) {
+      exists = this.config.containsKey(
+        this.propertyConfig.getPropertiesDomain() + key);
+    } else {
+      exists = this.config.containsKey(key);
+    }
+    return exists;
+  }
+
+
+  /** Calls {@link PropertyConfig#setEnvironmentProperties(Properties)}. */
+  public void configure()
+  {
+    this.propertyConfig.setEnvironmentProperties(this.config);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/props/PropertyConfig.java b/src/main/java/edu/vt/middleware/ldap/props/PropertyConfig.java
new file mode 100644
index 0000000..c0c7e0e
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/props/PropertyConfig.java
@@ -0,0 +1,72 @@
+/*
+  $Id: PropertyConfig.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.props;
+
+import java.util.Hashtable;
+import java.util.Properties;
+
+/**
+ * <code>PropertyConfig</code> provides an interface for objects that can be
+ * configured with a <code>PropertyInvoker.</code>
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public interface PropertyConfig
+{
+
+
+  /**
+   * This returns the properties domain for this property config.
+   *
+   * @return  <code>String</code> properties domain
+   */
+  String getPropertiesDomain();
+
+
+  /**
+   * This returns whether the supplied property exists.
+   *
+   * @param  name  <code>String</code> to check
+   *
+   * @return  <code>boolean</code> whether the supplied property exists
+   */
+  boolean hasEnvironmentProperty(String name);
+
+
+  /**
+   * This adds environment properties to this object. If name or value is null,
+   * then this method does nothing.
+   *
+   * @param  name  <code>String</code> property name
+   * @param  value  <code>String</code> property value
+   */
+  void setEnvironmentProperties(String name, String value);
+
+
+  /**
+   * See {@link #setEnvironmentProperties(String,String)}.
+   *
+   * @param  properties  <code>Properties</code>
+   */
+  void setEnvironmentProperties(Properties properties);
+
+
+  /**
+   * See {@link #setEnvironmentProperties(String,String)}.
+   *
+   * @param  properties  <code>Hashtable</code>
+   */
+  void setEnvironmentProperties(Hashtable<String, String> properties);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/props/SimplePropertyInvoker.java b/src/main/java/edu/vt/middleware/ldap/props/SimplePropertyInvoker.java
new file mode 100644
index 0000000..5400398
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/props/SimplePropertyInvoker.java
@@ -0,0 +1,88 @@
+/*
+  $Id: SimplePropertyInvoker.java 1441 2010-07-01 16:55:43Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1441 $
+  Updated: $Date: 2010-07-01 17:55:43 +0100 (Thu, 01 Jul 2010) $
+*/
+package edu.vt.middleware.ldap.props;
+
+import java.lang.reflect.Array;
+
+/**
+ * <code>SimplePropertyInvoker</code> stores setter methods for a class to make
+ * method invocation of simple properties easier.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1441 $ $Date: 2010-07-01 17:55:43 +0100 (Thu, 01 Jul 2010) $
+ */
+public class SimplePropertyInvoker extends AbstractPropertyInvoker
+{
+
+
+  /**
+   * Creates a new <code>SimplePropertyInvoker</code> for the supplied class.
+   *
+   * @param  c  <code>Class</code> that has setter methods
+   */
+  public SimplePropertyInvoker(final Class<?> c)
+  {
+    this.initialize(c, "");
+  }
+
+
+  /** {@inheritDoc} */
+  protected Object convertValue(final Class<?> type, final String value)
+  {
+    Object newValue = value;
+    if (type != String.class) {
+      if (Class.class.isAssignableFrom(type)) {
+        if ("null".equals(value)) {
+          newValue = null;
+        } else {
+          newValue = createClass(value);
+        }
+      } else if (Class[].class.isAssignableFrom(type)) {
+        if ("null".equals(value)) {
+          newValue = null;
+        } else {
+          final String[] classes = value.split(",");
+          newValue = Array.newInstance(Class.class, classes.length);
+          for (int i = 0; i < classes.length; i++) {
+            Array.set(newValue, i, createClass(classes[i]));
+          }
+        }
+      } else if (type.isEnum()) {
+        for (Object o : type.getEnumConstants()) {
+          final Enum<?> e = (Enum<?>) o;
+          if (e.name().equals(value)) {
+            newValue = o;
+          }
+        }
+      } else if (String[].class == type) {
+        newValue = value.split(",");
+      } else if (Object[].class == type) {
+        newValue = value.split(",");
+      } else if (float.class == type) {
+        newValue = Float.parseFloat(value);
+      } else if (int.class == type) {
+        newValue = Integer.parseInt(value);
+      } else if (long.class == type) {
+        newValue = Long.parseLong(value);
+      } else if (short.class == type) {
+        newValue = Short.parseShort(value);
+      } else if (double.class == type) {
+        newValue = Double.parseDouble(value);
+      } else if (boolean.class == type) {
+        newValue = Boolean.valueOf(value);
+      }
+    }
+    return newValue;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/servlets/AttributeServlet.java b/src/main/java/edu/vt/middleware/ldap/servlets/AttributeServlet.java
new file mode 100644
index 0000000..a49ec45
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/servlets/AttributeServlet.java
@@ -0,0 +1,240 @@
+/*
+  $Id: AttributeServlet.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Iterator;
+import javax.naming.directory.SearchResult;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.bean.LdapAttribute;
+import edu.vt.middleware.ldap.bean.LdapBeanFactory;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import edu.vt.middleware.ldap.pool.BlockingLdapPool;
+import edu.vt.middleware.ldap.pool.DefaultLdapFactory;
+import edu.vt.middleware.ldap.pool.LdapPool;
+import edu.vt.middleware.ldap.pool.LdapPoolConfig;
+import edu.vt.middleware.ldap.pool.SharedLdapPool;
+import edu.vt.middleware.ldap.pool.SoftLimitLdapPool;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>AttributeServlet</code> is a servlet which queries an LDAP and returns
+ * the value of a single attribute. Example:
+ * http://www.server.com/Attribute?query=uid=dfisher&attr=givenName If you need
+ * to pass complex queries, such as (&(cn=daniel*)(surname=fisher)), then the
+ * query must be form encoded. The content returned by the servlet is of type
+ * text/plain, if you want to receive the content as application/octet-stream
+ * that can be specified by passing the content-type=octet param. The following
+ * init params can be set for this servlet:
+ * edu.vt.middleware.ldap.servlets.propertiesFile - to load ldap properties from
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class AttributeServlet extends HttpServlet
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -5918353780927139315L;
+
+  /** Types of available pools. */
+  private enum PoolType {
+
+    /** blocking. */
+    BLOCKING,
+
+    /** soft limit. */
+    SOFTLIMIT,
+
+    /** shared. */
+    SHARED
+  }
+
+  /** Log for this class. */
+  private final Log logger = LogFactory.getLog(AttributeServlet.class);
+
+  /** Ldap bean factory. */
+  private LdapBeanFactory beanFactory = LdapBeanProvider.getLdapBeanFactory();
+
+  /** Pool to use for searching. */
+  private LdapPool<Ldap> pool;
+
+
+  /**
+   * Initialize this servlet.
+   *
+   * @param  config  <code>ServletConfig</code>
+   *
+   * @throws  ServletException  if an error occurs
+   */
+  public void init(final ServletConfig config)
+    throws ServletException
+  {
+    super.init(config);
+
+    final String propertiesFile = getInitParameter(
+      ServletConstants.PROPERTIES_FILE);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        ServletConstants.PROPERTIES_FILE + " = " + propertiesFile);
+    }
+
+    final LdapConfig ldapConfig = LdapConfig.createFromProperties(
+      AttributeServlet.class.getResourceAsStream(propertiesFile));
+
+    final String poolPropertiesFile = getInitParameter(
+      ServletConstants.POOL_PROPERTIES_FILE);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        ServletConstants.POOL_PROPERTIES_FILE + " = " + poolPropertiesFile);
+    }
+
+    final LdapPoolConfig ldapPoolConfig = LdapPoolConfig.createFromProperties(
+      AttributeServlet.class.getResourceAsStream(poolPropertiesFile));
+
+    final String poolType = getInitParameter(ServletConstants.POOL_TYPE);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(ServletConstants.POOL_TYPE + " = " + poolType);
+    }
+    if (PoolType.BLOCKING == PoolType.valueOf(poolType)) {
+      this.pool = new BlockingLdapPool(
+        ldapPoolConfig,
+        new DefaultLdapFactory(ldapConfig));
+    } else if (PoolType.SOFTLIMIT == PoolType.valueOf(poolType)) {
+      this.pool = new SoftLimitLdapPool(
+        ldapPoolConfig,
+        new DefaultLdapFactory(ldapConfig));
+    } else if (PoolType.SHARED == PoolType.valueOf(poolType)) {
+      this.pool = new SharedLdapPool(
+        ldapPoolConfig,
+        new DefaultLdapFactory(ldapConfig));
+    } else {
+      throw new ServletException("Unknown pool type: " + poolType);
+    }
+    this.pool.initialize();
+
+    final String beanFactoryClass = getInitParameter(
+      ServletConstants.BEAN_FACTORY);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(ServletConstants.BEAN_FACTORY + " = " + beanFactory);
+    }
+    if (beanFactoryClass != null) {
+      try {
+        this.beanFactory = (LdapBeanFactory) Class.forName(beanFactoryClass)
+            .newInstance();
+      } catch (ClassNotFoundException e) {
+        throw new ServletException(e);
+      } catch (InstantiationException e) {
+        throw new ServletException(e);
+      } catch (IllegalAccessException e) {
+        throw new ServletException(e);
+      }
+    }
+  }
+
+
+  /**
+   * Handle all requests sent to this servlet.
+   *
+   * @param  request  <code>HttpServletRequest</code>
+   * @param  response  <code>HttpServletResponse</code>
+   *
+   * @throws  ServletException  if an error occurs
+   * @throws  IOException  if an error occurs
+   */
+  public void service(
+    final HttpServletRequest request,
+    final HttpServletResponse response)
+    throws ServletException, IOException
+  {
+    final String attribute = request.getParameter("attr");
+    byte[] value = null;
+    final String content = request.getParameter("content-type");
+
+    if (content != null && content.equalsIgnoreCase("octet")) {
+      response.setContentType("application/octet-stream");
+      response.setHeader(
+        "Content-Disposition",
+        "attachment; filename=\"" + attribute + ".bin\"");
+    } else {
+      response.setContentType("text/plain");
+    }
+
+    try {
+      Ldap ldap = null;
+      try {
+        ldap = this.pool.checkOut();
+
+        final Iterator<SearchResult> i = ldap.search(
+          new SearchFilter(request.getParameter("query")),
+          request.getParameterValues("attr"));
+
+        final LdapResult r = this.beanFactory.newLdapResult();
+        r.addEntries(i);
+        for (LdapEntry e : r.getEntries()) {
+          final LdapAttribute a = e.getLdapAttributes().getAttribute(attribute);
+          if (a != null && a.getValues().size() > 0) {
+            final Object rawValue = a.getValues().iterator().next();
+            if (rawValue instanceof String) {
+              final String stringValue = (String) rawValue;
+              value = stringValue.getBytes();
+            } else {
+              value = (byte[]) rawValue;
+            }
+          }
+        }
+      } finally {
+        this.pool.checkIn(ldap);
+      }
+
+      if (value != null) {
+        final OutputStream out = response.getOutputStream();
+        out.write(value);
+        out.flush();
+        out.close();
+      }
+
+    } catch (Exception e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error performing search", e);
+      }
+      throw new ServletException(e.getMessage());
+    }
+  }
+
+
+  /**
+   * Called by the servlet container to indicate to a servlet that the servlet
+   * is being taken out of service.
+   */
+  public void destroy()
+  {
+    try {
+      this.pool.close();
+    } finally {
+      super.destroy();
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/servlets/CommonServlet.java b/src/main/java/edu/vt/middleware/ldap/servlets/CommonServlet.java
new file mode 100644
index 0000000..c87c850
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/servlets/CommonServlet.java
@@ -0,0 +1,108 @@
+/*
+  $Id: CommonServlet.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import edu.vt.middleware.ldap.servlets.session.SessionManager;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>CommonServlet</code> contains common code that each servlet uses to
+ * initialize itself.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class CommonServlet extends HttpServlet
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -2580419817969949661L;
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Used to manage a session after login and logout. */
+  protected SessionManager sessionManager;
+
+
+  /**
+   * Initialize this servlet.
+   *
+   * @param  config  <code>ServletConfig</code>
+   *
+   * @throws  ServletException  if an error occurs
+   */
+  public void init(final ServletConfig config)
+    throws ServletException
+  {
+    super.init(config);
+
+    String sessionManagerClass = getInitParameter(
+      ServletConstants.SESSION_MANAGER);
+    if (sessionManagerClass == null) {
+      sessionManagerClass = ServletConstants.DEFAULT_SESSION_MANAGER;
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        ServletConstants.SESSION_MANAGER + " = " + sessionManagerClass);
+    }
+    try {
+      this.sessionManager = (SessionManager) Class.forName(sessionManagerClass)
+          .newInstance();
+    } catch (ClassNotFoundException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Could not find class " + sessionManagerClass, e);
+      }
+      throw new ServletException(e);
+    } catch (InstantiationException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error(
+          "Could not instantiate class " + sessionManagerClass,
+          e);
+      }
+      throw new ServletException(e);
+    } catch (IllegalAccessException e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Could not access class " + sessionManagerClass, e);
+      }
+      throw new ServletException(e);
+    }
+
+    String sessionId = getInitParameter(ServletConstants.SESSION_ID);
+    if (sessionId == null) {
+      sessionId = ServletConstants.DEFAULT_SESSION_ID;
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(ServletConstants.SESSION_ID + " = " + sessionId);
+    }
+    this.sessionManager.setSessionId(sessionId);
+
+
+    String invalidateSession = getInitParameter(
+      ServletConstants.INVALIDATE_SESSION);
+    if (invalidateSession == null) {
+      invalidateSession = ServletConstants.DEFAULT_INVALIDATE_SESSION;
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        ServletConstants.INVALIDATE_SESSION + " = " + invalidateSession);
+    }
+    this.sessionManager.setInvalidateSession(
+      Boolean.valueOf(invalidateSession).booleanValue());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/servlets/LoginServlet.java b/src/main/java/edu/vt/middleware/ldap/servlets/LoginServlet.java
new file mode 100644
index 0000000..50e67cc
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/servlets/LoginServlet.java
@@ -0,0 +1,215 @@
+/*
+  $Id: LoginServlet.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import edu.vt.middleware.ldap.auth.Authenticator;
+import edu.vt.middleware.ldap.props.LdapProperties;
+
+/**
+ * <code>LoginServet</code> attempts to authenticate a user against an LDAP. The
+ * following init params can be set for this servlet:
+ * edu.vt.middleware.ldap.servlets.propertiesFile - to load authenticator
+ * properties from edu.vt.middleware.ldap.servlets.sessionId - to set the user
+ * identifier in the session edu.vt.middleware.ldap.servlets.loginUrl - to set
+ * the URL of your login page edu.vt.middleware.ldap.servlets.errorMsg - to
+ * display if authentication fails
+ * edu.vt.middleware.ldap.servlets.sessionManager - optional class to perform
+ * session management after login and logout (must extend
+ * edu.vt.middleware.ldap.servlets.session.SessionManager)
+ *
+ * <p>The following http params can be sent to this servlet: user - user
+ * identifier to authenticate credential - user credential to authenticate with
+ * url - to redirect client to after successful authentication</p>
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class LoginServlet extends CommonServlet
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -3482852409544351134L;
+
+  /** URL of the page that does collects user credentials. */
+  private String loginUrl;
+
+  /** Message to display if authentication fails. */
+  private String errorMsg;
+
+  /** Used to authenticate against an LDAP. */
+  private Authenticator auth;
+
+
+  /**
+   * Initialize this servlet.
+   *
+   * @param  config  <code>ServletConfig</code>
+   *
+   * @throws  ServletException  if an error occurs
+   */
+  public void init(final ServletConfig config)
+    throws ServletException
+  {
+    super.init(config);
+    this.loginUrl = getInitParameter(ServletConstants.LOGIN_URL);
+    if (this.loginUrl == null) {
+      this.loginUrl = ServletConstants.DEFAULT_LOGIN_URL;
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(ServletConstants.LOGIN_URL + " = " + this.loginUrl);
+    }
+    this.errorMsg = getInitParameter(ServletConstants.ERROR_MSG);
+    if (this.errorMsg == null) {
+      this.errorMsg = ServletConstants.DEFAULT_ERROR_MSG;
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(ServletConstants.ERROR_MSG + " = " + this.errorMsg);
+    }
+
+    String propertiesFile = getInitParameter(ServletConstants.PROPERTIES_FILE);
+    if (propertiesFile == null) {
+      propertiesFile = LdapProperties.PROPERTIES_FILE;
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        ServletConstants.PROPERTIES_FILE + " = " + propertiesFile);
+    }
+    this.auth = new Authenticator();
+    this.auth.loadFromProperties(
+      LoginServlet.class.getResourceAsStream(propertiesFile));
+  }
+
+
+  /**
+   * Handle all requests sent to this servlet.
+   *
+   * @param  request  <code>HttpServletRequest</code>
+   * @param  response  <code>HttpServletResponse</code>
+   *
+   * @throws  ServletException  if this request cannot be serviced
+   * @throws  IOException  if a response cannot be sent
+   */
+  public void service(
+    final HttpServletRequest request,
+    final HttpServletResponse response)
+    throws ServletException, IOException
+  {
+    boolean validCredentials = false;
+    String user = request.getParameter(ServletConstants.USER_PARAM);
+    if (user != null) {
+      user = user.trim().toLowerCase();
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Received user param = " + user);
+    }
+
+    final String credential = request.getParameter(
+      ServletConstants.CREDENTIAL_PARAM);
+    String url = request.getParameter(ServletConstants.URL_PARAM);
+    if (url == null) {
+      url = "";
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Received url param = " + url);
+    }
+
+    final StringBuffer error = new StringBuffer(this.errorMsg);
+
+    try {
+      if (this.auth.authenticate(user, credential)) {
+        validCredentials = true;
+      }
+    } catch (Exception e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error authenticating user " + user, e);
+      }
+      if (
+        e.getCause() != null &&
+          e.getCause().getMessage() != null &&
+          !"null".equals(e.getCause().getMessage())) {
+        error.append(": ").append(e.getCause().getMessage());
+      } else if (e.getMessage() != null && !"null".equals(e.getMessage())) {
+        error.append(": ").append(e.getMessage());
+      }
+    }
+
+    if (validCredentials) {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Authentication succeeded for user " + user);
+      }
+      try {
+        // invalidate existing session
+        HttpSession session = request.getSession(false);
+        if (session != null) {
+          session.invalidate();
+        }
+        session = request.getSession(true);
+        this.sessionManager.login(session, user);
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Initialized session for user " + user);
+        }
+        response.sendRedirect(url);
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Redirected user to " + url);
+        }
+        return;
+      } catch (Exception e) {
+        if (this.logger.isErrorEnabled()) {
+          this.logger.error("Error authorizing user " + user, e);
+        }
+        if (
+          e.getCause() != null &&
+            e.getCause().getMessage() != null &&
+            !"null".equals(e.getCause().getMessage())) {
+          error.append(": ").append(e.getCause().getMessage());
+        } else if (e.getMessage() != null && !"null".equals(e.getMessage())) {
+          error.append(": ").append(e.getMessage());
+        }
+      }
+    }
+
+    final StringBuffer errorUrl = new StringBuffer(this.loginUrl);
+    if (error != null) {
+      errorUrl.append("?error=").append(
+        URLEncoder.encode(error.toString(), "UTF-8"));
+    }
+    if (user != null) {
+      errorUrl.append("&user=").append(URLEncoder.encode(user, "UTF-8"));
+    }
+    if (url != null) {
+      errorUrl.append("&url=").append(URLEncoder.encode(url, "UTF-8"));
+    }
+    response.sendRedirect(errorUrl.toString());
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Redirected user to " + errorUrl.toString());
+    }
+  }
+
+
+  /**
+   * Called by the servlet container to indicate to a servlet that the servlet
+   * is being taken out of service.
+   */
+  public void destroy()
+  {
+    super.destroy();
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/servlets/LogoutServlet.java b/src/main/java/edu/vt/middleware/ldap/servlets/LogoutServlet.java
new file mode 100644
index 0000000..b9de933
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/servlets/LogoutServlet.java
@@ -0,0 +1,92 @@
+/*
+  $Id: LogoutServlet.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+import java.io.IOException;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * <code>LogoutServet</code> removes the session id attribute set by the <code>
+ * LoginServlet</code>. The following init params can be set for this servlet:
+ * edu.vt.middleware.ldap.servlets.sessionId - to remove from the session
+ *
+ * <p>The following http params can be sent to this servlet: url - to redirect
+ * client to after logout</p>
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class LogoutServlet extends CommonServlet
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = -6521700995773675507L;
+
+
+  /**
+   * Initialize this servlet.
+   *
+   * @param  config  <code>ServletConfig</code>
+   *
+   * @throws  ServletException  if an error occurs
+   */
+  public void init(final ServletConfig config)
+    throws ServletException
+  {
+    super.init(config);
+  }
+
+
+  /**
+   * Handle all requests sent to this servlet.
+   *
+   * @param  request  <code>HttpServletRequest</code>
+   * @param  response  <code>HttpServletResponse</code>
+   *
+   * @throws  ServletException  if this request cannot be serviced
+   * @throws  IOException  if a response cannot be sent
+   */
+  public void service(
+    final HttpServletRequest request,
+    final HttpServletResponse response)
+    throws ServletException, IOException
+  {
+    String url = request.getParameter(ServletConstants.URL_PARAM);
+    if (url == null) {
+      url = "";
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Received url param = " + url);
+    }
+
+    this.sessionManager.logout(request.getSession(true));
+    response.sendRedirect(url);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Redirected user to " + url);
+    }
+  }
+
+
+  /**
+   * Called by the servlet container to indicate to a servlet that the servlet
+   * is being taken out of service.
+   */
+  public void destroy()
+  {
+    super.destroy();
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/servlets/SearchServlet.java b/src/main/java/edu/vt/middleware/ldap/servlets/SearchServlet.java
new file mode 100644
index 0000000..7c98d47
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/servlets/SearchServlet.java
@@ -0,0 +1,254 @@
+/*
+  $Id: SearchServlet.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.dsml.DsmlSearch;
+import edu.vt.middleware.ldap.ldif.LdifSearch;
+import edu.vt.middleware.ldap.pool.BlockingLdapPool;
+import edu.vt.middleware.ldap.pool.DefaultLdapFactory;
+import edu.vt.middleware.ldap.pool.LdapPool;
+import edu.vt.middleware.ldap.pool.LdapPoolConfig;
+import edu.vt.middleware.ldap.pool.SharedLdapPool;
+import edu.vt.middleware.ldap.pool.SoftLimitLdapPool;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>SearchServlet</code> is a servlet which queries an LDAP and returns the
+ * result as LDIF or DSML. The following init params can be set for this
+ * servlet: edu.vt.middleware.ldap.servlets.propertiesFile - to load ldap
+ * properties from edu.vt.middleware.ldap.servlets.outputFormat - type of output
+ * to produce, 'ldif' or 'dsml' Example:
+ * http://www.server.com/Search?query=uid=dfisher If you need to pass complex
+ * queries, such as (&(cn=daniel*)(surname=fisher)), then the query must be form
+ * encoded. If you only want to receive a subset of attributes those can be
+ * specified. Example:
+ * http://www.server.com/Search?query=uid=dfisher&attrs=givenname&attrs=surname
+ *
+ * <h3>LDIF</h3>
+ *
+ * <p>The content returned by the servlet is of type text/plain.</p>
+ * <hr/>
+ * <h3>DSML</h3>
+ *
+ * <p>The content returned by the servlet is of type text/xml, if you want to
+ * receive the content as text/plain that can be specified as well. Example:
+ * http://www.server.com/Search?query=uid=dfisher&content-type=text By default
+ * DSML version 1 is returned, if you want to receive DSML version 2 then you
+ * must pass in the dsml-version parameter. Example:
+ * http://www.server.com/Search?query=uid=dfisher&dsml-version=2</p>
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class SearchServlet extends HttpServlet
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = 1731614499970954068L;
+
+  /** Types of available pools. */
+  private enum PoolType {
+
+    /** blocking. */
+    BLOCKING,
+
+    /** soft limit. */
+    SOFTLIMIT,
+
+    /** shared. */
+    SHARED
+  }
+
+  /** Types of available output. */
+  private enum OutputType {
+
+    /** LDIF output type. */
+    LDIF,
+
+    /** DSML output type. */
+    DSML
+  }
+
+  /** Log for this class. */
+  private final Log logger = LogFactory.getLog(SearchServlet.class);
+
+  /** Type of output to produce. */
+  private OutputType output;
+
+  /** Object to use for searching. */
+  private LdifSearch ldifSearch;
+
+  /** Object to use for searching. */
+  private DsmlSearch dsmlv1Search;
+
+  /** Object to use for searching. */
+  private DsmlSearch dsmlv2Search;
+
+
+  /**
+   * Initialize this servlet.
+   *
+   * @param  config  <code>ServletConfig</code>
+   *
+   * @throws  ServletException  if an error occurs
+   */
+  public void init(final ServletConfig config)
+    throws ServletException
+  {
+    super.init(config);
+
+    final String propertiesFile = getInitParameter(
+      ServletConstants.PROPERTIES_FILE);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        ServletConstants.PROPERTIES_FILE + " = " + propertiesFile);
+    }
+
+    final LdapConfig ldapConfig = LdapConfig.createFromProperties(
+      SearchServlet.class.getResourceAsStream(propertiesFile));
+
+    final String poolPropertiesFile = getInitParameter(
+      ServletConstants.POOL_PROPERTIES_FILE);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        ServletConstants.POOL_PROPERTIES_FILE + " = " + poolPropertiesFile);
+    }
+
+    final LdapPoolConfig ldapPoolConfig = LdapPoolConfig.createFromProperties(
+      SearchServlet.class.getResourceAsStream(poolPropertiesFile));
+
+    LdapPool<Ldap> ldapPool = null;
+    final String poolType = getInitParameter(ServletConstants.POOL_TYPE);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(ServletConstants.POOL_TYPE + " = " + poolType);
+    }
+    if (PoolType.BLOCKING == PoolType.valueOf(poolType)) {
+      ldapPool = new BlockingLdapPool(
+        ldapPoolConfig,
+        new DefaultLdapFactory(ldapConfig));
+    } else if (PoolType.SOFTLIMIT == PoolType.valueOf(poolType)) {
+      ldapPool = new SoftLimitLdapPool(
+        ldapPoolConfig,
+        new DefaultLdapFactory(ldapConfig));
+    } else if (PoolType.SHARED == PoolType.valueOf(poolType)) {
+      ldapPool = new SharedLdapPool(
+        ldapPoolConfig,
+        new DefaultLdapFactory(ldapConfig));
+    } else {
+      throw new ServletException("Unknown pool type: " + poolType);
+    }
+    ldapPool.initialize();
+
+    this.ldifSearch = new LdifSearch(ldapPool);
+    this.dsmlv1Search = new DsmlSearch(ldapPool);
+    this.dsmlv1Search.setVersion(DsmlSearch.Version.ONE);
+    this.dsmlv2Search = new DsmlSearch(ldapPool);
+    this.dsmlv2Search.setVersion(DsmlSearch.Version.TWO);
+
+    String outputType = getInitParameter(ServletConstants.OUTPUT_FORMAT);
+    if (outputType == null) {
+      outputType = ServletConstants.DEFAULT_OUTPUT_FORMAT;
+    }
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(ServletConstants.OUTPUT_FORMAT + " = " + outputType);
+    }
+    this.output = OutputType.valueOf(outputType);
+  }
+
+
+  /**
+   * Handle all requests sent to this servlet.
+   *
+   * @param  request  <code>HttpServletRequest</code>
+   * @param  response  <code>HttpServletResponse</code>
+   *
+   * @throws  ServletException  if an error occurs
+   * @throws  IOException  if an error occurs
+   */
+  public void service(
+    final HttpServletRequest request,
+    final HttpServletResponse response)
+    throws ServletException, IOException
+  {
+    if (this.logger.isInfoEnabled()) {
+      this.logger.info(
+        "Performing search: " + request.getParameter("query") +
+        " for attributes: " + request.getParameter("attrs"));
+    }
+    try {
+      if (this.output == OutputType.LDIF) {
+        response.setContentType("text/plain");
+        this.ldifSearch.search(
+          request.getParameter("query"),
+          request.getParameterValues("attrs"),
+          new BufferedWriter(
+            new OutputStreamWriter(response.getOutputStream())));
+      } else {
+        final String content = request.getParameter("content-type");
+        if (content != null && content.equalsIgnoreCase("text")) {
+          response.setContentType("text/plain");
+        } else {
+          response.setContentType("text/xml");
+        }
+
+        final String dsmlVersion = request.getParameter("dsml-version");
+        if ("2".equals(dsmlVersion)) {
+          this.dsmlv2Search.search(
+            request.getParameter("query"),
+            request.getParameterValues("attrs"),
+            new BufferedWriter(
+              new OutputStreamWriter(response.getOutputStream())));
+        } else {
+          this.dsmlv1Search.search(
+            request.getParameter("query"),
+            request.getParameterValues("attrs"),
+            new BufferedWriter(
+              new OutputStreamWriter(response.getOutputStream())));
+        }
+      }
+    } catch (Exception e) {
+      if (this.logger.isErrorEnabled()) {
+        this.logger.error("Error performing search", e);
+      }
+      throw new ServletException(e);
+    }
+  }
+
+
+  /**
+   * Called by the servlet container to indicate to a servlet that the servlet
+   * is being taken out of service.
+   */
+  public void destroy()
+  {
+    try {
+      // all search instances share the same pool
+      // only need to close one of them
+      this.ldifSearch.close();
+    } finally {
+      super.destroy();
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/servlets/ServletConstants.java b/src/main/java/edu/vt/middleware/ldap/servlets/ServletConstants.java
new file mode 100644
index 0000000..fcc64b9
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/servlets/ServletConstants.java
@@ -0,0 +1,108 @@
+/*
+  $Id: ServletConstants.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+/**
+ * <code>ServletConstants</code> contains all the constants needed by the ldap
+ * servlet package.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public final class ServletConstants
+{
+
+  /** Domain to look for properties in, value is {@value}. */
+  public static final String PROPERTIES_DOMAIN =
+    "edu.vt.middleware.ldap.servlets.";
+
+  /** LDAP initialization properties file, value is {@value}. */
+  public static final String PROPERTIES_FILE = PROPERTIES_DOMAIN +
+    "propertiesFile";
+
+  /** LDAP pool initialization properties file, value is {@value}. */
+  public static final String POOL_PROPERTIES_FILE = PROPERTIES_DOMAIN +
+    "poolPropertiesFile";
+
+  /** Format of search output, value is {@value}. */
+  public static final String OUTPUT_FORMAT = PROPERTIES_DOMAIN + "outputFormat";
+
+  /** Default format of search output, value is {@value}. */
+  public static final String DEFAULT_OUTPUT_FORMAT = "DSML";
+
+  /** Type of pool used, value is {@value}. */
+  public static final String POOL_TYPE = PROPERTIES_DOMAIN + "poolType";
+
+  /** Type of ldap bean factory, value is {@value}. */
+  public static final String BEAN_FACTORY = PROPERTIES_DOMAIN + "beanFactory";
+
+  /**
+   * Identifier to set in the session after valid authentication, value is
+   * {@value}.
+   */
+  public static final String SESSION_ID = PROPERTIES_DOMAIN + "sessionId";
+
+  /**
+   * Default identifier to set in the session after valid authentication, value
+   * is {@value}.
+   */
+  public static final String DEFAULT_SESSION_ID = "user";
+
+  /** Whether to invalidate the user session at logout, value is {@value}. */
+  public static final String INVALIDATE_SESSION = PROPERTIES_DOMAIN +
+    "invalidateSession";
+
+  /**
+   * Default behavior for invalidating the user session at logout, value is
+   * {@value}.
+   */
+  public static final String DEFAULT_INVALIDATE_SESSION = "true";
+
+  /** URL of the page that collects user credentials, value is {@value}. */
+  public static final String LOGIN_URL = PROPERTIES_DOMAIN + "loginUrl";
+
+  /**
+   * Default URL of the page that does collects user credentials, value is
+   * {@value}.
+   */
+  public static final String DEFAULT_LOGIN_URL = "/";
+
+  /** Error message to display if authentication fails, value is {@value}. */
+  public static final String ERROR_MSG = PROPERTIES_DOMAIN + "errorMsg";
+
+  /** Class used to initialize http sessions. */
+  public static final String SESSION_MANAGER = PROPERTIES_DOMAIN +
+    "sessionManager";
+
+  /** Default session initializer, value is {@value}. */
+  public static final String DEFAULT_SESSION_MANAGER =
+    "edu.vt.middleware.ldap.servlets.session.DefaultSessionManager";
+
+  /** Default error message, value is {@value}. */
+  public static final String DEFAULT_ERROR_MSG =
+    "Could not authenticate or authorize user";
+
+  /** HTTP parameter used to transmit the user identifier, value is {@value}. */
+  public static final String USER_PARAM = "user";
+
+  /** HTTP parameter used to transmit the user credential, value is {@value}. */
+  public static final String CREDENTIAL_PARAM = "credential";
+
+  /** HTTP parameter used to transmit the redirect url, value is {@value}. */
+  public static final String URL_PARAM = "url";
+
+
+  /** Default constructor. */
+  private ServletConstants() {}
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/servlets/session/DefaultSessionManager.java b/src/main/java/edu/vt/middleware/ldap/servlets/session/DefaultSessionManager.java
new file mode 100644
index 0000000..51ed4d1
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/servlets/session/DefaultSessionManager.java
@@ -0,0 +1,97 @@
+/*
+  $Id: DefaultSessionManager.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets.session;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpSession;
+
+/**
+ * <code>DefaultSessionManager</code> provides a base class for session
+ * management. After a successful authentication, this class sets the session id
+ * that is set for the login servlet to the user name. After logout the session
+ * id attribute is removed from the session. This class is used by default if no
+ * custom session manager has been set.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class DefaultSessionManager extends SessionManager
+{
+
+
+  /**
+   * This performs any actions necessary to login the suppled user.
+   *
+   * @param  session  <code>HttpSession</code>
+   * @param  user  <code>String</code>
+   *
+   * @throws  ServletException  if an error occurs initializing the session
+   */
+  public void login(final HttpSession session, final String user)
+    throws ServletException
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Begin login method");
+    }
+    if (this.sessionId != null) {
+      session.setAttribute(this.sessionId, user);
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug(
+          "Set session attribute " + this.sessionId + " to " + user);
+      }
+    } else {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Could not set session attribute, value is null");
+      }
+    }
+  }
+
+
+  /**
+   * This performs any actions necessary to logout the suppled session.
+   *
+   * @param  session  <code>HttpSession</code>
+   *
+   * @throws  ServletException  if an error occurs cleaning up the session
+   */
+  public void logout(final HttpSession session)
+    throws ServletException
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Begin logout method");
+    }
+    if (this.sessionId != null) {
+      final String user = (String) session.getAttribute(this.sessionId);
+      session.removeAttribute(this.sessionId);
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug(
+          "Removed session attribute " + this.sessionId + " for " + user);
+      }
+    } else {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Could not remove session attribute, value is null");
+      }
+    }
+    if (this.invalidateSession) {
+      session.invalidate();
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Session invalidated");
+      }
+    } else {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Session was not invalidated");
+      }
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/servlets/session/SessionManager.java b/src/main/java/edu/vt/middleware/ldap/servlets/session/SessionManager.java
new file mode 100644
index 0000000..0d99b4f
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/servlets/session/SessionManager.java
@@ -0,0 +1,92 @@
+/*
+  $Id: SessionManager.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets.session;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpSession;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>SessionManager</code> provides a parent class for initializing a <code>
+ * HttpSession</code> after a successful authentication and destroying a <code>
+ * HttpSession</code> after logout.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public abstract class SessionManager
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Identifier to set in the session after valid authentication. */
+  protected String sessionId;
+
+  /** Whether to invalidate session on logout. */
+  protected boolean invalidateSession = true;
+
+
+  /**
+   * This sets a session id that can be used in {@link #login} or {@link
+   * #logout}.
+   *
+   * @param  id  <code>String</code>
+   */
+  public void setSessionId(final String id)
+  {
+    this.sessionId = id;
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Set session attribute to " + this.sessionId);
+    }
+  }
+
+
+  /**
+   * This sets whether to invalidate a session on logout. Default value is true.
+   *
+   * @param  invalidate  <code>boolean</code>
+   */
+  public void setInvalidateSession(final boolean invalidate)
+  {
+    this.invalidateSession = invalidate;
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Set invalidateSession to " + this.invalidateSession);
+    }
+  }
+
+
+  /**
+   * This performs any actions necessary to login the suppled user.
+   *
+   * @param  session  <code>HttpSession</code>
+   * @param  user  <code>String</code>
+   *
+   * @throws  ServletException  if an error occurs initializing the session
+   */
+  public abstract void login(HttpSession session, String user)
+    throws ServletException;
+
+
+  /**
+   * This performs any actions necessary to logout the suppled session.
+   *
+   * @param  session  <code>HttpSession</code>
+   *
+   * @throws  ServletException  if an error occurs cleaning up the session
+   */
+  public abstract void logout(HttpSession session)
+    throws ServletException;
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/AbstractCredentialReader.java b/src/main/java/edu/vt/middleware/ldap/ssl/AbstractCredentialReader.java
new file mode 100644
index 0000000..c4e37f9
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/AbstractCredentialReader.java
@@ -0,0 +1,111 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Base class for all credential readers. It provides support for loading files
+ * from resources on the classpath or a filepath. If a path is prefixed with the
+ * string "classpath:" it is interpreted as a classpath specification. If a path
+ * is prefixed with the string "file:" it is interpreted as a file path. Any
+ * other input throws IllegalArgumentException.
+ *
+ * @param  <T>  Type of credential read by this instance.
+ *
+ * @author  Middleware Services
+ * @version  $Revision$
+ */
+public abstract class AbstractCredentialReader<T> implements CredentialReader<T>
+{
+
+  /** Prefix used to indicate a classpath resource. */
+  public static final String CLASSPATH_PREFIX = "classpath:";
+
+  /** Prefix used to indicate a file resource. */
+  public static final String FILE_PREFIX = "file:";
+
+  /** Start index of path specification when given a classpath resource. */
+  private static final int CLASSPATH_START_INDEX = CLASSPATH_PREFIX.length();
+
+  /** Start index of path specification when given a file resource. */
+  private static final int FILE_START_INDEX = FILE_PREFIX.length();
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+
+  /** {@inheritDoc} */
+  public T read(final String path, final String... params)
+    throws IOException, GeneralSecurityException
+  {
+    InputStream is = null;
+    if (path.startsWith(CLASSPATH_PREFIX)) {
+      is = getClass().getResourceAsStream(
+        path.substring(CLASSPATH_START_INDEX));
+    } else if (path.startsWith(FILE_PREFIX)) {
+      is = new FileInputStream(new File(path.substring(FILE_START_INDEX)));
+    } else {
+      throw new IllegalArgumentException(
+        "path must start with either " + CLASSPATH_PREFIX + " or " +
+        FILE_PREFIX);
+    }
+    if (is != null) {
+      try {
+        return read(is, params);
+      } finally {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("Successfully loaded " + path);
+        }
+        is.close();
+      }
+    } else {
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("Failed to load " + path);
+      }
+      return null;
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract T read(InputStream is, String... params)
+    throws IOException, GeneralSecurityException;
+
+
+  /**
+   * Gets a buffered input stream from the given input stream. If the given
+   * instance is already buffered, it is simply returned.
+   *
+   * @param  is  Input stream from which to create buffered instance.
+   *
+   * @return  Buffered input stream. If the given instance is already buffered,
+   * it is simply returned.
+   */
+  protected InputStream getBufferedInputStream(final InputStream is)
+  {
+    if (is.markSupported()) {
+      return is;
+    } else {
+      return new BufferedInputStream(is);
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/AbstractSSLContextInitializer.java b/src/main/java/edu/vt/middleware/ldap/ssl/AbstractSSLContextInitializer.java
new file mode 100644
index 0000000..8531b0e
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/AbstractSSLContextInitializer.java
@@ -0,0 +1,55 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Provides common implementation for <code>SSLContextInitializer</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public abstract class AbstractSSLContextInitializer
+  implements SSLContextInitializer
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+
+  /** {@inheritDoc} */
+  public SSLContext initSSLContext(final String protocol)
+    throws GeneralSecurityException
+  {
+    final SSLContext ctx = SSLContext.getInstance(protocol);
+    ctx.init(this.getKeyManagers(), this.getTrustManagers(), null);
+    return ctx;
+  }
+
+
+  /** {@inheritDoc} */
+  public abstract TrustManager[] getTrustManagers()
+    throws GeneralSecurityException;
+
+
+  /** {@inheritDoc} */
+  public abstract KeyManager[] getKeyManagers()
+    throws GeneralSecurityException;
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/AbstractTLSSocketFactory.java b/src/main/java/edu/vt/middleware/ldap/ssl/AbstractTLSSocketFactory.java
new file mode 100644
index 0000000..d60a97f
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/AbstractTLSSocketFactory.java
@@ -0,0 +1,371 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Provides common implementation for <code>TLSSocketFactory</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public abstract class AbstractTLSSocketFactory extends SSLSocketFactory
+{
+
+  /** Default SSL protocol, value is {@value}. */
+  public static final String DEFAULT_PROTOCOL = "TLS";
+
+  /** SSLSocketFactory used for creating SSL sockets. */
+  protected SSLSocketFactory factory;
+
+  /** Hostname verifier for this socket factory. */
+  protected HostnameVerifier hostnameVerifier;
+
+  /** Enabled cipher suites. */
+  protected String[] cipherSuites;
+
+  /** Enabled protocol versions. */
+  protected String[] protocols;
+
+
+  /**
+   * Prepares this socket factory for use. Must be called before factory can be
+   * used.
+   *
+   * @throws  GeneralSecurityException  if the factory cannot be initialized
+   */
+  public abstract void initialize()
+    throws GeneralSecurityException;
+
+
+  /**
+   * This returns the underlying <code>SSLSocketFactory</code> that this class
+   * uses for creating SSL Sockets.
+   *
+   * @return  <code>SSLSocketFactory</code>
+   */
+  public SSLSocketFactory getFactory()
+  {
+    return this.factory;
+  }
+
+
+  /**
+   * Returns the hostname verifier to invoke when sockets are created.
+   *
+   * @return  hostname verifier
+   */
+  public HostnameVerifier getHostnameVerifier()
+  {
+    return hostnameVerifier;
+  }
+
+
+  /**
+   * Sets the hostname verifier to invoke when sockets are created.
+   *
+   * @param  verifier  for SSL hostnames
+   */
+  public void setHostnameVerifier(final HostnameVerifier verifier)
+  {
+    hostnameVerifier = verifier;
+  }
+
+
+  /**
+   * This returns the names of the SSL cipher suites which are currently enabled
+   * for use on sockets created by this factory. A null value indicates that no
+   * specific cipher suites have been enabled and that the default suites are in
+   * use.
+   *
+   * @return  <code>String[]</code> of cipher suites
+   */
+  public String[] getEnabledCipherSuites()
+  {
+    return this.cipherSuites;
+  }
+
+
+  /**
+   * Sets the cipher suites enabled for use on sockets created by this factory.
+   * See {@link javax.net.ssl.SSLSocket#setEnabledCipherSuites(String[])}.
+   *
+   * @param  s  <code>String[]</code> of cipher suites
+   */
+  public void setEnabledCipherSuites(final String[] s)
+  {
+    this.cipherSuites = s;
+  }
+
+
+  /**
+   * This returns the names of the protocol versions which are currently enabled
+   * for use on sockets created by this factory. A null value indicates that no
+   * specific protocols have been enabled and that the default protocols are in
+   * use.
+   *
+   * @return  <code>String[]</code> of protocols
+   */
+  public String[] getEnabledProtocols()
+  {
+    return this.protocols;
+  }
+
+
+  /**
+   * Sets the protocol versions enabled for use on sockets created by this
+   * factory. See {@link javax.net.ssl.SSLSocket#setEnabledProtocols(String[])}.
+   *
+   * @param  s  <code>String[]</code> of cipher suites
+   */
+  public void setEnabledProtocols(final String[] s)
+  {
+    this.protocols = s;
+  }
+
+
+  /**
+   * Initializes the supplied socket for use.
+   *
+   * @param  s  <code>SSLSocket</code> to initialize
+   *
+   * @return  <code>SSLSocket</code>
+   *
+   * @throws  IOException  if an I/O error occurs when initializing the socket
+   */
+  protected SSLSocket initSSLSocket(final SSLSocket s)
+    throws IOException
+  {
+    if (this.cipherSuites != null) {
+      s.setEnabledCipherSuites(this.cipherSuites);
+    }
+    if (this.protocols != null) {
+      s.setEnabledProtocols(this.protocols);
+    }
+    if (hostnameVerifier != null) {
+      // calling getSession() will initiate the handshake if necessary
+      final String hostname = s.getSession().getPeerHost();
+      if (!hostnameVerifier.verify(hostname, s.getSession())) {
+        s.close();
+        s.getSession().invalidate();
+        throw new SSLPeerUnverifiedException(
+          String.format(
+            "Hostname '%s' does not match the hostname in the server's " +
+            "certificate", hostname));
+      }
+    }
+    return s;
+  }
+
+
+  /**
+   * This returns a socket layered over an existing socket connected to the
+   * named host, at the given port.
+   *
+   * @param  s  <code>Socket</code> existing socket
+   * @param  host  <code>String</code> server hostname
+   * @param  port  <code>int</code> server port
+   * @param  autoClose  <code>boolean</code> close the underlying socket when
+   * this socket is closed
+   *
+   * @return  <code>Socket</code> - connected to the specified host and port
+   *
+   * @throws  IOException  if an I/O error occurs when creating the socket
+   */
+  public Socket createSocket(
+    final Socket s,
+    final String host,
+    final int port,
+    final boolean autoClose)
+    throws IOException
+  {
+    SSLSocket socket = null;
+    if (this.factory != null) {
+      socket = this.initSSLSocket(
+        (SSLSocket) this.factory.createSocket(s, host, port, autoClose));
+    }
+    return socket;
+  }
+
+
+  /**
+   * This creates an unconnected socket.
+   *
+   * @return  <code>Socket</code> - unconnected socket
+   *
+   * @throws  IOException  if an I/O error occurs when creating the socket
+   */
+  public Socket createSocket()
+    throws IOException
+  {
+    SSLSocket socket = null;
+    if (this.factory != null) {
+      socket = this.initSSLSocket((SSLSocket) this.factory.createSocket());
+    }
+    return socket;
+  }
+
+
+  /**
+   * This creates a socket and connects it to the specified port number at the
+   * specified address.
+   *
+   * @param  host  <code>InetAddress</code> server hostname
+   * @param  port  <code>int</code> server port
+   *
+   * @return  <code>Socket</code> - connected to the specified host and port
+   *
+   * @throws  IOException  if an I/O error occurs when creating the socket
+   */
+  public Socket createSocket(final InetAddress host, final int port)
+    throws IOException
+  {
+    SSLSocket socket = null;
+    if (this.factory != null) {
+      socket = this.initSSLSocket(
+        (SSLSocket) this.factory.createSocket(host, port));
+    }
+    return socket;
+  }
+
+
+  /**
+   * This creates a socket and connect it to the specified port number at the
+   * specified address. The socket will also be bound to the supplied local
+   * address and port.
+   *
+   * @param  address  <code>InetAddress</code> server hostname
+   * @param  port  <code>int</code> server port
+   * @param  localAddress  <code>InetAddress</code> client hostname
+   * @param  localPort  <code>int</code> client port
+   *
+   * @return  <code>Socket</code> - connected to the specified host and port
+   *
+   * @throws  IOException  if an I/O error occurs when creating the socket
+   */
+  public Socket createSocket(
+    final InetAddress address,
+    final int port,
+    final InetAddress localAddress,
+    final int localPort)
+    throws IOException
+  {
+    SSLSocket socket = null;
+    if (this.factory != null) {
+      socket = this.initSSLSocket(
+        (SSLSocket) this.factory.createSocket(
+          address,
+          port,
+          localAddress,
+          localPort));
+    }
+    return socket;
+  }
+
+
+  /**
+   * This creates a socket and connects it to the specified port number at the
+   * specified address.
+   *
+   * @param  host  <code>String</code> server hostname
+   * @param  port  <code>int</code> server port
+   *
+   * @return  <code>Socket</code> - connected to the specified host and port
+   *
+   * @throws  IOException  if an I/O error occurs when creating the socket
+   */
+  public Socket createSocket(final String host, final int port)
+    throws IOException
+  {
+    SSLSocket socket = null;
+    if (this.factory != null) {
+      socket = this.initSSLSocket(
+        (SSLSocket) this.factory.createSocket(host, port));
+    }
+    return socket;
+  }
+
+
+  /**
+   * This creates a socket and connect it to the specified port number at the
+   * specified address. The socket will also be bound to the supplied local
+   * address and port.
+   *
+   * @param  host  <code>String</code> server hostname
+   * @param  port  <code>int</code> server port
+   * @param  localHost  <code>InetAddress</code> client hostname
+   * @param  localPort  <code>int</code> client port
+   *
+   * @return  <code>Socket</code> - connected to the specified host and port
+   *
+   * @throws  IOException  if an I/O error occurs when creating the socket
+   */
+  public Socket createSocket(
+    final String host,
+    final int port,
+    final InetAddress localHost,
+    final int localPort)
+    throws IOException
+  {
+    SSLSocket socket = null;
+    if (this.factory != null) {
+      socket = this.initSSLSocket(
+        (SSLSocket) this.factory.createSocket(
+          host,
+          port,
+          localHost,
+          localPort));
+    }
+    return socket;
+  }
+
+
+  /**
+   * This returns the list of cipher suites which are enabled by default.
+   *
+   * @return  <code>String[]</code> - array of the cipher suites
+   */
+  public String[] getDefaultCipherSuites()
+  {
+    String[] ciphers = null;
+    if (this.factory != null) {
+      ciphers = this.factory.getDefaultCipherSuites();
+    }
+    return ciphers;
+  }
+
+
+  /**
+   * This returns the names of the cipher suites which could be enabled for use
+   * on an SSL connection.
+   *
+   * @return  <code>String[]</code> - array of the cipher suites
+   */
+  public String[] getSupportedCipherSuites()
+  {
+    String[] ciphers = null;
+    if (this.factory != null) {
+      ciphers = this.factory.getSupportedCipherSuites();
+    }
+    return ciphers;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/AggregateTrustManager.java b/src/main/java/edu/vt/middleware/ldap/ssl/AggregateTrustManager.java
new file mode 100644
index 0000000..39a9d20
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/AggregateTrustManager.java
@@ -0,0 +1,99 @@
+/*
+  $Id: AggregateTrustManager.java 2231 2012-02-02 15:46:27Z dfisher $
+
+  Copyright (C) 2003-2012 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2231 $
+  Updated: $Date: 2012-02-02 15:46:27 +0000 (Thu, 02 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.X509TrustManager;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Trust manager that delegates to multiple trust managers.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2231 $
+ */
+public class AggregateTrustManager implements X509TrustManager
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Trust managers to invoke. */
+  private X509TrustManager[] trustManagers;
+
+
+  /**
+   * Creates a new aggregate trust manager.
+   *
+   * @param managers  to aggregate
+   */
+  public AggregateTrustManager(final X509TrustManager... managers)
+  {
+    trustManagers = managers;
+  }
+
+
+  /** {@inheritDoc} */
+  public void checkClientTrusted(
+    final X509Certificate[] chain, final String authType)
+    throws CertificateException
+  {
+    if (trustManagers != null) {
+      for (X509TrustManager tm : trustManagers) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("invoking checkClientTrusted for " + tm);
+        }
+        tm.checkClientTrusted(chain, authType);
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public void checkServerTrusted(
+    final X509Certificate[] chain, final String authType)
+    throws CertificateException
+  {
+    if (trustManagers != null) {
+      for (X509TrustManager tm : trustManagers) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("invoking checkServerTrusted for " + tm);
+        }
+        tm.checkServerTrusted(chain, authType);
+      }
+    }
+  }
+
+
+  /** {@inheritDoc} */
+  public X509Certificate[] getAcceptedIssuers()
+  {
+    final List<X509Certificate> issuers = new ArrayList<X509Certificate>();
+    if (trustManagers != null) {
+      for (X509TrustManager tm : trustManagers) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("invoking getAcceptedIssuers invoked for " + tm);
+        }
+        for (X509Certificate cert : tm.getAcceptedIssuers()) {
+          issuers.add(cert);
+        }
+      }
+    }
+    return issuers.toArray(new X509Certificate[issuers.size()]);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/CertificateHostnameVerifier.java b/src/main/java/edu/vt/middleware/ldap/ssl/CertificateHostnameVerifier.java
new file mode 100644
index 0000000..5fbfaa3
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/CertificateHostnameVerifier.java
@@ -0,0 +1,37 @@
+/*
+  $Id: CertificateHostnameVerifier.java 2231 2012-02-02 15:46:27Z dfisher $
+
+  Copyright (C) 2003-2012 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2231 $
+  Updated: $Date: 2012-02-02 15:46:27 +0000 (Thu, 02 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * Interface for verifying a hostname matching a certificate.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2231 $
+ */
+public interface CertificateHostnameVerifier
+{
+
+
+  /**
+   * Verify the supplied hostname matches the supplied certificate.
+   *
+   * @param  hostname  to verify
+   * @param  cert  to verify hostname against
+   *
+   * @return  whether hostname is valid for the supplied certificate
+   */
+  boolean verify(final String hostname, final X509Certificate cert);
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/CredentialConfig.java b/src/main/java/edu/vt/middleware/ldap/ssl/CredentialConfig.java
new file mode 100644
index 0000000..00c5a54
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/CredentialConfig.java
@@ -0,0 +1,43 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * <code>CredentialConfig</code> provides a base interface for all credential
+ * configurations. Since credential configs are invoked via reflection by the
+ * PropertyInvoker their method signatures are not important. They only need to
+ * be able to create an SSL context initializer once their properties have been
+ * set.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public interface CredentialConfig
+{
+
+
+  /**
+   * Creates an <code>SSLContextInitializer</code> using the configured trust
+   * and authentication material in this config.
+   *
+   * @return  <code>SSLContextInitializer</code>
+   *
+   * @throws  GeneralSecurityException  if the ssl context initializer cannot be
+   * created
+   */
+  SSLContextInitializer createSSLContextInitializer()
+    throws GeneralSecurityException;
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/CredentialConfigParser.java b/src/main/java/edu/vt/middleware/ldap/ssl/CredentialConfigParser.java
new file mode 100644
index 0000000..b0dfc59
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/CredentialConfigParser.java
@@ -0,0 +1,205 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import edu.vt.middleware.ldap.props.SimplePropertyInvoker;
+
+/**
+ * Parses the configuration data associated with credential configs and ssl
+ * socket factories. The format of the property string should be like:
+ *
+ * <pre>
+   MySSLSocketFactory
+     {KeyStoreCredentialConfig
+       {{trustStore=/tmp/my.truststore}{trustStoreType=JKS}}}
+ * </pre>
+ *
+ * <p>or</p>
+ *
+ * <pre>
+   {KeyStoreCredentialConfig
+     {{trustStore=/tmp/my.truststore}{trustStoreType=JKS}}}
+ * </pre>
+ *
+ * <p>or</p>
+ *
+ * <pre>
+   {{trustCertificates=/tmp/my.crt}}
+ * </pre>
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 930 $ $Date: 2009-10-26 16:44:26 -0400 (Mon, 26 Oct 2009) $
+ */
+public class CredentialConfigParser
+{
+
+  /** Property string for configuring a credential config. */
+  private static final Pattern FULL_CONFIG_PATTERN = Pattern.compile(
+    "([^\\{]+)\\s*\\{\\s*([^\\{]+)\\s*\\{\\s*(.*)\\}\\s*\\}\\s*");
+
+  /** Property string for configuring a credential config. */
+  private static final Pattern CREDENTIAL_ONLY_CONFIG_PATTERN = Pattern.compile(
+    "\\s*\\{\\s*([^\\{]+)\\s*\\{\\s*(.*)\\}\\s*\\}\\s*");
+
+  /** Property string for configuring a credential config. */
+  private static final Pattern PARAMS_ONLY_CONFIG_PATTERN = Pattern.compile(
+    "\\s*\\{\\s*(.*)\\s*\\}\\s*");
+
+  /** Pattern for finding properties. */
+  private static final Pattern PROPERTY_PATTERN = Pattern.compile(
+    "([^\\}\\{])+");
+
+  /** SSL socket factory class found in the config. */
+  private String sslSocketFactoryClassName =
+    "edu.vt.middleware.ldap.ssl.TLSSocketFactory";
+
+  /** Credential config class found in the config. */
+  private String credentialConfigClassName =
+    "edu.vt.middleware.ldap.ssl.X509CredentialConfig";
+
+  /** Properties found in the config to set on the credential config. */
+  private Map<String, String> properties = new HashMap<String, String>();
+
+
+  /**
+   * Creates a new <code>CredentialConfigParser</code> with the supplied
+   * configuration string.
+   *
+   * @param  config  <code>String</code>
+   */
+  public CredentialConfigParser(final String config)
+  {
+    final Matcher fullMatcher = FULL_CONFIG_PATTERN.matcher(config);
+    final Matcher credentialOnlyMatcher = CREDENTIAL_ONLY_CONFIG_PATTERN
+        .matcher(config);
+    final Matcher paramsOnlyMatcher = PARAMS_ONLY_CONFIG_PATTERN.matcher(
+      config);
+    Matcher m = null;
+    if (fullMatcher.matches()) {
+      int i = 1;
+      this.sslSocketFactoryClassName = fullMatcher.group(i++).trim();
+      this.credentialConfigClassName = fullMatcher.group(i++).trim();
+      if (!"".equals(fullMatcher.group(i).trim())) {
+        m = PROPERTY_PATTERN.matcher(fullMatcher.group(i).trim());
+      }
+    } else if (credentialOnlyMatcher.matches()) {
+      int i = 1;
+      this.credentialConfigClassName = credentialOnlyMatcher.group(i++).trim();
+      if (!"".equals(credentialOnlyMatcher.group(i).trim())) {
+        m = PROPERTY_PATTERN.matcher(credentialOnlyMatcher.group(i).trim());
+      }
+    } else if (paramsOnlyMatcher.matches()) {
+      final int i = 1;
+      if (!"".equals(paramsOnlyMatcher.group(i).trim())) {
+        m = PROPERTY_PATTERN.matcher(paramsOnlyMatcher.group(i).trim());
+      }
+    }
+    if (m != null) {
+      while (m.find()) {
+        final String input = m.group().trim();
+        if (input != null && !"".equals(input)) {
+          final String[] s = input.split("=");
+          this.properties.put(s[0].trim(), s[1].trim());
+        }
+      }
+    }
+  }
+
+
+  /**
+   * Returns the SSL socket factory class name from the configuration.
+   *
+   * @return  <code>String</code> class name
+   */
+  public String getSslSocketFactoryClassName()
+  {
+    return this.sslSocketFactoryClassName;
+  }
+
+
+  /**
+   * Returns the credential config class name from the configuration.
+   *
+   * @return  <code>String</code> class name
+   */
+  public String getCredentialConfigClassName()
+  {
+    return this.credentialConfigClassName;
+  }
+
+
+  /**
+   * Returns the properties from the configuration.
+   *
+   * @return  <code>Map</code> of property name to value
+   */
+  public Map<String, String> getProperties()
+  {
+    return this.properties;
+  }
+
+
+  /**
+   * Returns whether the supplied configuration data contains a credential
+   * config.
+   *
+   * @param  config  <code>String</code>
+   *
+   * @return  <code>boolean</code>
+   */
+  public static boolean isCredentialConfig(final String config)
+  {
+    return
+      FULL_CONFIG_PATTERN.matcher(config).matches() ||
+        CREDENTIAL_ONLY_CONFIG_PATTERN.matcher(config).matches() ||
+        PARAMS_ONLY_CONFIG_PATTERN.matcher(config).matches();
+  }
+
+
+  /**
+   * Initialize an instance of credential config with the properties contained
+   * in this config.
+   *
+   * @return  <code>Object</code> of the type <code>CredentialConfig</code>
+   */
+  public Object initializeType()
+  {
+    final Class<?> c = SimplePropertyInvoker.createClass(
+      this.getCredentialConfigClassName());
+    final Object o = SimplePropertyInvoker.instantiateType(
+      c,
+      this.getCredentialConfigClassName());
+    this.setProperties(c, o);
+    return o;
+  }
+
+
+  /**
+   * Sets the properties on the supplied object.
+   *
+   * @param  c  <code>Class</code> type of the supplied object
+   * @param  o  <code>Object</code> to invoke properties on
+   */
+  protected void setProperties(final Class<?> c, final Object o)
+  {
+    final SimplePropertyInvoker invoker = new SimplePropertyInvoker(c);
+    for (Map.Entry<String, String> entry : this.getProperties().entrySet()) {
+      invoker.setProperty(o, entry.getKey(), entry.getValue());
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/CredentialReader.java b/src/main/java/edu/vt/middleware/ldap/ssl/CredentialReader.java
new file mode 100644
index 0000000..6f1429d
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/CredentialReader.java
@@ -0,0 +1,62 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+
+/**
+ * Reads a credential from an IO source.
+ *
+ * @param  <T>  Type of credential read by this instance.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public interface CredentialReader<T>
+{
+
+
+  /**
+   * Reads a credential object from a path.
+   *
+   * @param  path  Path from which to read credential.
+   * @param  params  Arbitrary string parameters, e.g. password, needed to read
+   * the credential.
+   *
+   * @return  Credential read from data at path.
+   *
+   * @throws  IOException  On IO errors.
+   * @throws  GeneralSecurityException  On errors with the credential data.
+   */
+  T read(String path, String... params)
+    throws IOException, GeneralSecurityException;
+
+
+  /**
+   * Reads a credential object from an input stream.
+   *
+   * @param  is  Input stream from which to read credential.
+   * @param  params  Arbitrary string parameters, e.g. password, needed to read
+   * the credential.
+   *
+   * @return  Credential read from data in stream.
+   *
+   * @throws  IOException  On IO errors.
+   * @throws  GeneralSecurityException  On errors with the credential data.
+   */
+  T read(InputStream is, String... params)
+    throws IOException, GeneralSecurityException;
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/DefaultHostnameVerifier.java b/src/main/java/edu/vt/middleware/ldap/ssl/DefaultHostnameVerifier.java
new file mode 100644
index 0000000..01a0912
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/DefaultHostnameVerifier.java
@@ -0,0 +1,355 @@
+/*
+  $Id: DefaultHostnameVerifier.java 2231 2012-02-02 15:46:27Z dfisher $
+
+  Copyright (C) 2003-2012 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2231 $
+  Updated: $Date: 2012-02-02 15:46:27 +0000 (Thu, 02 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+import javax.net.SocketFactory;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import edu.vt.middleware.ldap.LdapUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Hostname verifier that provides an implementation similar to what occurs with
+ * JNDI startTLS. Verification occurs in the following order:
+ * <ul>
+ *  <li>if hostname is IP, then cert must have exact match IP subjAltName</li>
+ *  <li>hostname must match any DNS subjAltName if any exist</li>
+ *  <li>hostname must match the first CN</li>
+ *  <li>if cert begins with a wildcard, domains are used for matching</li>
+ * </ul>
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2231 $ $Date: 2012-02-02 15:46:27 +0000 (Thu, 02 Feb 2012) $
+ */
+public class DefaultHostnameVerifier
+  implements HostnameVerifier, CertificateHostnameVerifier
+{
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** Enum for subject alt name types. */
+  private enum SubjectAltNameType
+  {
+    /** other name (0). */
+    OTHER_NAME,
+
+    /** ref822 name (1). */
+    RFC822_NAME,
+
+    /** dns name (2). */
+    DNS_NAME,
+
+    /** x400 address (3). */
+    X400_ADDRESS,
+
+    /** directory name (4). */
+    DIRECTORY_NAME,
+
+    /** edi party name (5). */
+    EDI_PARTY_NAME,
+
+    /** uniform resource identifier (6). */
+    UNIFORM_RESOURCE_IDENTIFIER,
+
+    /** ip address (7). */
+    IP_ADDRESS,
+
+    /** registered id (8). */
+    REGISTERED_ID;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean verify(final String hostname, final SSLSession session)
+  {
+    boolean b = false;
+    try {
+      String name = null;
+      if (hostname != null) {
+        // if IPv6 strip off the "[]"
+        if (hostname.startsWith("[") && hostname.endsWith("]")) {
+          name = hostname.substring(1, hostname.length() - 1).trim();
+        } else {
+          name = hostname.trim();
+        }
+      }
+      b = verify(name, (X509Certificate) session.getPeerCertificates()[0]);
+    } catch (SSLPeerUnverifiedException e) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("Could not get certificate from the SSL session", e);
+      }
+    }
+    return b;
+  }
+
+
+  /**
+   * Verify if the hostname is an IP address using
+   * {@link LdapUtil#isIPAddress(String)}. Delegates to
+   * {@link #verifyIP(String, X509Certificate)} and
+   * {@link #verifyDNS(String, X509Certificate)} accordingly.
+   *
+   * @param  hostname  to verify
+   * @param  cert  to verify hostname against
+   *
+   * @return  whether hostname is valid for the supplied certificate
+   */
+  public boolean verify(final String hostname, final X509Certificate cert)
+  {
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug("Verify with the following parameters:");
+      this.logger.debug("  hostname = " + hostname);
+      this.logger.debug(
+        "  cert = " + cert.getSubjectX500Principal().toString());
+    }
+    boolean b = false;
+    if (LdapUtil.isIPAddress(hostname)) {
+      b = verifyIP(hostname, cert);
+    } else {
+      b = verifyDNS(hostname, cert);
+    }
+    return b;
+  }
+
+
+  /**
+   * Verify the certificate allows use of the supplied IP address.
+   *
+   * From RFC2818:
+   * In some cases, the URI is specified as an IP address rather than a
+   * hostname. In this case, the iPAddress subjectAltName must be present
+   * in the certificate and must exactly match the IP in the URI.
+   *
+   * @param  ip  address to match in the certificate
+   * @param   cert  to inspect for the IP address
+   *
+   * @return  whether the ip matched a subject alt name
+   */
+  protected boolean verifyIP(final String ip, final X509Certificate cert)
+  {
+    final String[] subjAltNames = getSubjectAltNames(
+      cert, SubjectAltNameType.IP_ADDRESS);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        "verifyIP using subjectAltNames = " + Arrays.toString(subjAltNames));
+    }
+    for (String name : subjAltNames) {
+      if (ip.equalsIgnoreCase(name)) {
+        if (this.logger.isDebugEnabled()) {
+          this.logger.debug("verifyIP found hostname match: " + name);
+        }
+        return true;
+      }
+    }
+    return false;
+  }
+
+
+  /**
+   * Verify the certificate allows use of the supplied DNS name. Note that only
+   * the first CN is used.
+   *
+   * From RFC2818:
+   * If a subjectAltName extension of type dNSName is present, that MUST
+   * be used as the identity. Otherwise, the (most specific) Common Name
+   * field in the Subject field of the certificate MUST be used. Although
+   * the use of the Common Name is existing practice, it is deprecated and
+   * Certification Authorities are encouraged to use the dNSName instead.
+   *
+   * Matching is performed using the matching rules specified by
+   * [RFC2459].  If more than one identity of a given type is present in
+   * the certificate (e.g., more than one dNSName name, a match in any one
+   * of the set is considered acceptable.)
+   *
+   * @param  hostname  to match in the certificate
+   * @param  cert  to inspect for the hostname
+   *
+   * @return  whether the hostname matched a subject alt name or CN
+   */
+  protected boolean verifyDNS(final String hostname, final X509Certificate cert)
+  {
+    boolean verified = false;
+    final String[] subjAltNames = getSubjectAltNames(
+      cert, SubjectAltNameType.DNS_NAME);
+    if (this.logger.isDebugEnabled()) {
+      this.logger.debug(
+        "verifyDNS using subjectAltNames = " + Arrays.toString(subjAltNames));
+    }
+    if (subjAltNames.length > 0) {
+      // if subject alt names exist, one must match
+      for (String name : subjAltNames) {
+        if (isMatch(hostname, name)) {
+          if (this.logger.isDebugEnabled()) {
+            this.logger.debug("verifyDNS found hostname match: " + name);
+          }
+          verified = true;
+          break;
+        }
+      }
+    } else {
+      final String[] cns = getCNs(cert);
+      if (this.logger.isDebugEnabled()) {
+        this.logger.debug("verifyDNS using CN = " + Arrays.toString(cns));
+      }
+      if (cns.length > 0) {
+        if (isMatch(hostname, cns[0])) {
+          if (this.logger.isDebugEnabled()) {
+            this.logger.debug("verifyDNS found hostname match: " + cns[0]);
+          }
+          verified = true;
+        }
+      }
+    }
+    return verified;
+  }
+
+
+  /**
+   * Returns the subject alternative names matching the supplied name type from
+   * the supplied certificate.
+   *
+   * @param  cert  to get subject alt names from
+   * @param  type  subject alt name type
+   *
+   * @return  subject alt names
+   */
+  private String[] getSubjectAltNames(
+    final X509Certificate cert, final SubjectAltNameType type)
+  {
+    final List<String> names = new ArrayList<String>();
+    try {
+      final Collection<List<?>> subjAltNames =
+        cert.getSubjectAlternativeNames();
+      if (subjAltNames != null) {
+        for (List<?> generalName : subjAltNames) {
+          final Integer nameType = (Integer) generalName.get(0);
+          if (nameType.intValue() == type.ordinal()) {
+            names.add((String) generalName.get(1));
+          }
+        }
+      }
+    } catch (CertificateParsingException e) {
+      if (this.logger.isWarnEnabled()) {
+        this.logger.warn("Error reading subject alt names from certificate", e);
+      }
+    }
+    return names.toArray(new String[names.size()]);
+  }
+
+
+  /**
+   * Returns the CNs from the supplied certificate.
+   *
+   * @param  cert  to get CNs from
+   *
+   * @return  CNs
+   */
+  private String[] getCNs(final X509Certificate cert)
+  {
+    final List<String> names = new ArrayList<String>();
+    // not a perfect implementation but appears to work for >99% of certificates
+    // and has the virtue of not requiring any dependencies
+    final String subjectPrincipal = cert.getSubjectX500Principal().toString();
+    final StringTokenizer st = new StringTokenizer(subjectPrincipal, ",");
+    while (st.hasMoreTokens()) {
+      final String tok = st.nextToken();
+      final int x = tok.indexOf("CN=");
+      if (x >= 0) {
+        names.add(tok.substring(x + "CN=".length()));
+      }
+    }
+    return names.toArray(new String[names.size()]);
+  }
+
+
+  /**
+   * Determines if the supplied hostname matches a name derived from the
+   * certificate. If the certificate name starts with '*', the domain components
+   * after the first '.' in each name are compared.
+   *
+   * @param  hostname  to match
+   * @param  certName  to match
+   *
+   * @return  whether the hostname matched the cert name
+   */
+  private boolean isMatch(final String hostname, final String certName)
+  {
+    // must start with '*' and contain two domain components
+    final boolean isWildcard =
+      certName.startsWith("*.") &&
+      certName.indexOf('.') < certName.lastIndexOf('.');
+
+    boolean match = false;
+    if (isWildcard) {
+      final String certNameDomain = certName.substring(certName.indexOf("."));
+
+      final int hostnameIdx = hostname.indexOf(".") != -1 ?
+        hostname.indexOf(".") : hostname.length();
+      final String hostnameDomain = hostname.substring(hostnameIdx);
+
+      match = certNameDomain.equalsIgnoreCase(hostnameDomain);
+    } else {
+      match = certName.equalsIgnoreCase(hostname);
+    }
+    return match;
+  }
+
+
+  /**
+   * Socket factory that uses {@link DefaultHostnameVerifier}.
+   */
+  public static class SSLSocketFactory extends TLSSocketFactory
+  {
+
+
+    /**
+     * Creates a new socket factory that uses this hostname verifier.
+     */
+    public SSLSocketFactory()
+    {
+      setHostnameVerifier(new DefaultHostnameVerifier());
+    }
+
+
+    /**
+     * Returns the default SSL socket factory.
+     *
+     * @return  socket factory
+     */
+    public static SocketFactory getDefault()
+    {
+      final SSLSocketFactory sf = new SSLSocketFactory();
+      try {
+        sf.initialize();
+      } catch (GeneralSecurityException e) {
+        final Log logger = LogFactory.getLog(TLSSocketFactory.class);
+        if (logger.isErrorEnabled()) {
+          logger.error("Error initializing socket factory", e);
+        }
+      }
+      return sf;
+    }
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/DefaultSSLContextInitializer.java b/src/main/java/edu/vt/middleware/ldap/ssl/DefaultSSLContextInitializer.java
new file mode 100644
index 0000000..1c70d87
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/DefaultSSLContextInitializer.java
@@ -0,0 +1,74 @@
+/*
+  $Id: DefaultSSLContextInitializer.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.TrustManager;
+
+/**
+ * Provides a default implementation of <code>SSLContextInitializer</code> which
+ * allows the setting of trust and key managers in order to create an SSL
+ * context.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class DefaultSSLContextInitializer extends AbstractSSLContextInitializer
+{
+
+  /** Trust managers. */
+  private TrustManager[] trustManagers;
+
+  /** Key managers. */
+  private KeyManager[] keyManagers;
+
+
+  /** {@inheritDoc} */
+  public TrustManager[] getTrustManagers()
+    throws GeneralSecurityException
+  {
+    return this.trustManagers;
+  }
+
+
+  /**
+   * Sets the trust managers.
+   *
+   * @param  tm  <code>TrustManager[]</code>
+   */
+  public void setTrustManagers(final TrustManager[] tm)
+  {
+    this.trustManagers = tm;
+  }
+
+
+  /** {@inheritDoc} */
+  public KeyManager[] getKeyManagers()
+    throws GeneralSecurityException
+  {
+    return this.keyManagers;
+  }
+
+
+  /**
+   * Sets the key managers.
+   *
+   * @param  km  <code>KeyManager[]</code>
+   */
+  public void setKeyManagers(final KeyManager[] km)
+  {
+    this.keyManagers = km;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/HostnameVerifyingTrustManager.java b/src/main/java/edu/vt/middleware/ldap/ssl/HostnameVerifyingTrustManager.java
new file mode 100644
index 0000000..775bbfd
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/HostnameVerifyingTrustManager.java
@@ -0,0 +1,99 @@
+/*
+  $Id: HostnameVerifyingTrustManager.java 2231 2012-02-02 15:46:27Z dfisher $
+
+  Copyright (C) 2003-2012 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2231 $
+  Updated: $Date: 2012-02-02 15:46:27 +0000 (Thu, 02 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Trust manager that delegates to {@link CertificateHostnameVerifier}. Any name
+ * that verifies passes this trust manager check.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2231 $
+ */
+public class HostnameVerifyingTrustManager implements X509TrustManager
+{
+
+  /** Hostnames to allow. */
+  private String[] hostnames;
+
+  /** Hostname verifier to use for trust. */
+  private CertificateHostnameVerifier hostnameVerifier;
+
+
+  /**
+   * Creates a new hostname verifying trust manager.
+   *
+   * @param  verifier  that establishes trust
+   * @param  names  to match against a certificate
+   */
+  public HostnameVerifyingTrustManager(
+    final CertificateHostnameVerifier verifier,
+    final String... names)
+  {
+    hostnameVerifier = verifier;
+    hostnames = names;
+  }
+
+
+  /** {@inheritDoc} */
+  public void checkClientTrusted(
+    final X509Certificate[] chain, final String authType)
+    throws CertificateException
+  {
+    checkCertificateTrusted(chain[0]);
+  }
+
+
+  /** {@inheritDoc} */
+  public void checkServerTrusted(
+    final X509Certificate[] chain, final String authType)
+    throws CertificateException
+  {
+    checkCertificateTrusted(chain[0]);
+  }
+
+
+  /**
+   * Verifies the supplied certificate using the hostname verifier with each
+   * hostname.
+   *
+   * @param  cert  to verify
+   *
+   * @throws  CertificateException  if none of the hostnames verify
+   */
+  private void checkCertificateTrusted(final X509Certificate cert)
+    throws CertificateException
+  {
+    for (String name : hostnames) {
+      if (hostnameVerifier.verify(name, cert)) {
+        return;
+      }
+    }
+    throw new CertificateException(
+      String.format(
+        "Hostname '%s' does not match the hostname in the server's " +
+        "certificate", Arrays.toString(hostnames)));
+  }
+
+
+  /** {@inheritDoc} */
+  public X509Certificate[] getAcceptedIssuers()
+  {
+    return new X509Certificate[0];
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreCredentialConfig.java b/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreCredentialConfig.java
new file mode 100644
index 0000000..a1d88aa
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreCredentialConfig.java
@@ -0,0 +1,213 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/**
+ * Provides the properties necessary for creating an SSL context initializer
+ * with a <code>KeyStoreCredentialReader</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public class KeyStoreCredentialConfig implements CredentialConfig
+{
+
+  /** Handles loading keystores. */
+  protected KeyStoreCredentialReader keyStoreReader =
+    new KeyStoreCredentialReader();
+
+  /** Name of the truststore to use for the SSL connection. */
+  private String trustStore;
+
+  /** Password needed to open the truststore. */
+  private String trustStorePassword;
+
+  /** Truststore type. */
+  private String trustStoreType;
+
+  /** Name of the keystore to use for the SSL connection. */
+  private String keyStore;
+
+  /** Password needed to open the keystore. */
+  private String keyStorePassword;
+
+  /** Keystore type. */
+  private String keyStoreType;
+
+
+  /**
+   * This returns the name of the truststore to use.
+   *
+   * @return  <code>String</code> truststore name
+   */
+  public String getTrustStore()
+  {
+    return this.trustStore;
+  }
+
+
+  /**
+   * This sets the name of the truststore to use.
+   *
+   * @param  s  <code>String</code> truststore name
+   */
+  public void setTrustStore(final String s)
+  {
+    this.trustStore = s;
+  }
+
+
+  /**
+   * This returns the password for the truststore.
+   *
+   * @return  <code>String</code> truststore password
+   */
+  public String getTrustStorePassword()
+  {
+    return this.trustStorePassword;
+  }
+
+
+  /**
+   * This sets the password for the truststore.
+   *
+   * @param  s  <code>String</code> truststore password
+   */
+  public void setTrustStorePassword(final String s)
+  {
+    this.trustStorePassword = s;
+  }
+
+
+  /**
+   * This returns the type of the truststore.
+   *
+   * @return  <code>String</code> truststore type
+   */
+  public String getTrustStoreType()
+  {
+    return this.trustStoreType;
+  }
+
+
+  /**
+   * This sets the type of the truststore.
+   *
+   * @param  s  <code>String</code> truststore type
+   */
+  public void setTrustStoreType(final String s)
+  {
+    this.trustStoreType = s;
+  }
+
+
+  /**
+   * This returns the name of the keystore to use.
+   *
+   * @return  <code>String</code> keystore name
+   */
+  public String getKeyStore()
+  {
+    return this.keyStore;
+  }
+
+
+  /**
+   * This sets the name of the keystore to use.
+   *
+   * @param  s  <code>String</code> keystore name
+   */
+  public void setKeyStore(final String s)
+  {
+    this.keyStore = s;
+  }
+
+
+  /**
+   * This returns the password for the keystore.
+   *
+   * @return  <code>String</code> keystore password
+   */
+  public String getKeyStorePassword()
+  {
+    return this.keyStorePassword;
+  }
+
+
+  /**
+   * This sets the password for the keystore.
+   *
+   * @param  s  <code>String</code> keystore password
+   */
+  public void setKeyStorePassword(final String s)
+  {
+    this.keyStorePassword = s;
+  }
+
+
+  /**
+   * This returns the type of the keystore.
+   *
+   * @return  <code>String</code> keystore type
+   */
+  public String getKeyStoreType()
+  {
+    return this.keyStoreType;
+  }
+
+
+  /**
+   * This sets the type of the keystore.
+   *
+   * @param  s  <code>String</code> keystore type
+   */
+  public void setKeyStoreType(final String s)
+  {
+    this.keyStoreType = s;
+  }
+
+
+  /** {@inheritDoc} */
+  public SSLContextInitializer createSSLContextInitializer()
+    throws GeneralSecurityException
+  {
+    final KeyStoreSSLContextInitializer sslInit =
+      new KeyStoreSSLContextInitializer();
+    try {
+      if (this.trustStore != null) {
+        sslInit.setTrustKeystore(
+          this.keyStoreReader.read(
+            this.trustStore,
+            this.trustStorePassword,
+            this.trustStoreType));
+      }
+      if (this.keyStore != null) {
+        sslInit.setAuthenticationKeystore(
+          this.keyStoreReader.read(
+            this.keyStore,
+            this.keyStorePassword,
+            this.keyStoreType));
+        sslInit.setAuthenticationPassword(
+          this.keyStorePassword != null ? this.keyStorePassword.toCharArray()
+                                        : null);
+      }
+    } catch (IOException e) {
+      throw new GeneralSecurityException(e);
+    }
+    return sslInit;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreCredentialReader.java b/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreCredentialReader.java
new file mode 100644
index 0000000..a073d01
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreCredentialReader.java
@@ -0,0 +1,74 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Arrays;
+
+/**
+ * Reads keystore credentials from a classpath, filepath, or stream resource.
+ *
+ * @author  Middleware Services
+ * @version  $Revision$
+ */
+public class KeyStoreCredentialReader extends AbstractCredentialReader<KeyStore>
+{
+
+
+  /**
+   * Reads a keystore from an input stream.
+   *
+   * @param  is  Input stream from which to read keystore.
+   * @param  params  Two optional parameters are supported:
+   *
+   * <ul>
+   *   <li>KeyStore password</li>
+   *   <li>KeyStore type; defaults to JVM default keystore format if
+   *     omitted</li>
+   * </ul>
+   *
+   * <p>If only a single parameter is supplied, it is assumed to be
+   * the password.</p>
+   *
+   * @return  KeyStore read from data in stream.
+   *
+   * @throws  IOException  On IO errors.
+   * @throws  GeneralSecurityException  On errors with the credential data.
+   */
+  public KeyStore read(final InputStream is, final String... params)
+    throws IOException, GeneralSecurityException
+  {
+    char[] password = null;
+    if (params.length > 0 && params[0] != null) {
+      password = params[0].toCharArray();
+    }
+
+    String type = KeyStore.getDefaultType();
+    if (params.length > 1 && params[1] != null) {
+      type = params[1];
+    }
+
+    final KeyStore keystore = KeyStore.getInstance(type);
+    if (is != null) {
+      keystore.load(this.getBufferedInputStream(is), password);
+      if (password != null) {
+        Arrays.fill(password, '0');
+      }
+    }
+    return keystore;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreSSLContextInitializer.java b/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreSSLContextInitializer.java
new file mode 100644
index 0000000..31e8f49
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/KeyStoreSSLContextInitializer.java
@@ -0,0 +1,106 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+/**
+ * Provides a <code>SSLContextInitializer</code> which can use java KeyStores to
+ * create key and trust managers.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public class KeyStoreSSLContextInitializer extends AbstractSSLContextInitializer
+{
+
+  /** KeyStore used to create trust managers. */
+  private KeyStore trustKeystore;
+
+  /** KeyStore used to create key managers. */
+  private KeyStore authenticationKeystore;
+
+  /** Password used to access the authentication keystore. */
+  private char[] authenticationPassword;
+
+
+  /**
+   * Sets the keystore to use for creating the trust managers.
+   *
+   * @param  ks  <code>KeyStore</code>
+   */
+  public void setTrustKeystore(final KeyStore ks)
+  {
+    this.trustKeystore = ks;
+  }
+
+
+  /**
+   * Sets the keystore to use for creating the key managers.
+   *
+   * @param  ks  <code>KeyStore</code>
+   */
+  public void setAuthenticationKeystore(final KeyStore ks)
+  {
+    this.authenticationKeystore = ks;
+  }
+
+
+  /**
+   * Sets the password used for accessing the authentication keystore.
+   *
+   * @param  password  <code>char[]</code>
+   */
+  public void setAuthenticationPassword(final char[] password)
+  {
+    this.authenticationPassword = password;
+  }
+
+
+  /** {@inheritDoc} */
+  public TrustManager[] getTrustManagers()
+    throws GeneralSecurityException
+  {
+    TrustManager[] tm = null;
+    if (this.trustKeystore != null) {
+      final TrustManagerFactory tmf = TrustManagerFactory.getInstance(
+        TrustManagerFactory.getDefaultAlgorithm());
+      tmf.init(this.trustKeystore);
+      tm = tmf.getTrustManagers();
+    }
+    return tm;
+  }
+
+
+  /** {@inheritDoc} */
+  public KeyManager[] getKeyManagers()
+    throws GeneralSecurityException
+  {
+    KeyManager[] km = null;
+    if (
+      this.authenticationKeystore != null &&
+        this.authenticationPassword != null) {
+      final KeyManagerFactory kmf = KeyManagerFactory.getInstance(
+        KeyManagerFactory.getDefaultAlgorithm());
+      kmf.init(this.authenticationKeystore, this.authenticationPassword);
+      km = kmf.getKeyManagers();
+    }
+    return km;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/PrivateKeyCredentialReader.java b/src/main/java/edu/vt/middleware/ldap/ssl/PrivateKeyCredentialReader.java
new file mode 100644
index 0000000..a018b3b
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/PrivateKeyCredentialReader.java
@@ -0,0 +1,61 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import edu.vt.middleware.ldap.LdapUtil;
+
+/**
+ * Reads private key credentials from classpath, filepath, or stream resource.
+ * Supported private key formats include: PKCS8.
+ *
+ * @author  Middleware Services
+ * @version  $Revision$
+ */
+public class PrivateKeyCredentialReader
+  extends AbstractCredentialReader<PrivateKey>
+{
+
+
+  /**
+   * Reads a private key from an input stream.
+   *
+   * @param  is  Input stream from which to read private key.
+   * @param  params  A single optional parameter, algorithm, may be specified.
+   * The default is RSA.
+   *
+   * @return  Private key read from data in stream.
+   *
+   * @throws  IOException  On IO errors.
+   * @throws  GeneralSecurityException  On errors with the credential data.
+   */
+  public PrivateKey read(final InputStream is, final String... params)
+    throws IOException, GeneralSecurityException
+  {
+    String algorithm = "RSA";
+    if (params.length > 0 && params[0] != null) {
+      algorithm = params[0];
+    }
+
+    final KeyFactory kf = KeyFactory.getInstance(algorithm);
+    final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(
+      LdapUtil.readInputStream(this.getBufferedInputStream(is)));
+    return kf.generatePrivate(spec);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/SSLContextInitializer.java b/src/main/java/edu/vt/middleware/ldap/ssl/SSLContextInitializer.java
new file mode 100644
index 0000000..a54a613
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/SSLContextInitializer.java
@@ -0,0 +1,66 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+
+/**
+ * Provides an interface for the initialization of new SSL contexts.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public interface SSLContextInitializer
+{
+
+
+  /**
+   * Creates an initialized SSLContext for the supplied protocol.
+   *
+   * @param  protocol  type to use for SSL
+   *
+   * @return  <code>SSLContext</code>
+   *
+   * @throws  GeneralSecurityException  if the SSLContext cannot be created
+   */
+  SSLContext initSSLContext(String protocol)
+    throws GeneralSecurityException;
+
+
+  /**
+   * Returns the trust managers used when creating SSL contexts.
+   *
+   * @return  <code>TrustManager[]</code>
+   *
+   * @throws  GeneralSecurityException  if an errors occurs while loading the
+   * TrustManagers
+   */
+  TrustManager[] getTrustManagers()
+    throws GeneralSecurityException;
+
+
+  /**
+   * Returns the key managers used when creating SSL contexts.
+   *
+   * @return  <code>KeyManagers[]</code>
+   *
+   * @throws  GeneralSecurityException  if an errors occurs while loading the
+   * KeyManagers
+   */
+  KeyManager[] getKeyManagers()
+    throws GeneralSecurityException;
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/SingletonTLSSocketFactory.java b/src/main/java/edu/vt/middleware/ldap/ssl/SingletonTLSSocketFactory.java
new file mode 100644
index 0000000..b72ebba
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/SingletonTLSSocketFactory.java
@@ -0,0 +1,75 @@
+/*
+  $Id: SingletonTLSSocketFactory.java 1742 2010-11-19 15:18:06Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1742 $
+  Updated: $Date: 2010-11-19 15:18:06 +0000 (Fri, 19 Nov 2010) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+import javax.net.SocketFactory;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * TLSSocketFactory implementation that uses a static SSLContextInitializer.
+ * Useful for SSL configurations that can only retrieve the SSLSocketFactory
+ * from getDefault().
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1742 $ $Date: 2010-11-19 15:18:06 +0000 (Fri, 19 Nov 2010) $
+ */
+public class SingletonTLSSocketFactory extends TLSSocketFactory
+{
+  /** SSLContextInitializer used for initializing SSL contexts. */
+  protected static SSLContextInitializer staticContextInitializer;
+
+
+  /** {@inheritDoc} */
+  public void setSSLContextInitializer(final SSLContextInitializer initializer)
+  {
+    if (staticContextInitializer != null) {
+      final Log logger = LogFactory.getLog(SingletonTLSSocketFactory.class);
+      if (logger.isWarnEnabled()) {
+        logger.warn("SSLContextInitializer is being overridden");
+      }
+    }
+    staticContextInitializer = initializer;
+  }
+
+
+  /** {@inheritDoc} */
+  public void initialize()
+    throws GeneralSecurityException
+  {
+    super.setSSLContextInitializer(staticContextInitializer);
+    super.initialize();
+  }
+
+
+  /**
+   * This returns the default SSL socket factory.
+   *
+   * @return  <code>SocketFactory</code>
+   */
+  public static SocketFactory getDefault()
+  {
+    final SingletonTLSSocketFactory sf = new SingletonTLSSocketFactory();
+    try {
+      sf.initialize();
+    } catch (GeneralSecurityException e) {
+      final Log logger = LogFactory.getLog(SingletonTLSSocketFactory.class);
+      if (logger.isErrorEnabled()) {
+        logger.error("Error initializing socket factory", e);
+      }
+    }
+    return sf;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/TLSSocketFactory.java b/src/main/java/edu/vt/middleware/ldap/ssl/TLSSocketFactory.java
new file mode 100644
index 0000000..37abf2b
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/TLSSocketFactory.java
@@ -0,0 +1,116 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <code>TLSSocketFactory</code> is an extension of SSLSocketFactory. Note that
+ * {@link #initialize()} must be called prior to using this socket factory. This
+ * means that this class cannot be passed to implementations that expect the
+ * socket factory to function immediately after construction.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public class TLSSocketFactory extends AbstractTLSSocketFactory
+{
+
+  /** SSLContextInitializer used for initializing SSL contexts. */
+  protected SSLContextInitializer contextInitializer =
+    new DefaultSSLContextInitializer();
+
+
+  /**
+   * Returns the SSL context initializer.
+   *
+   * @return  <code>SSLContextInitializer</code>
+   */
+  public SSLContextInitializer getSSLContextInitializer()
+  {
+    return this.contextInitializer;
+  }
+
+
+  /**
+   * Sets the SSL context initializer.
+   *
+   * @param  initializer  to create SSL contexts with
+   */
+  public void setSSLContextInitializer(final SSLContextInitializer initializer)
+  {
+    this.contextInitializer = initializer;
+  }
+
+
+  /**
+   * Creates the underlying SSLContext using truststore and keystore attributes
+   * and makes this factory ready for use. Must be called before factory can be
+   * used.
+   *
+   * @throws  GeneralSecurityException  if the SSLContext cannot be created
+   */
+  public void initialize()
+    throws GeneralSecurityException
+  {
+    final SSLContext ctx = this.getSSLContextInitializer().initSSLContext(
+      DEFAULT_PROTOCOL);
+    this.factory = ctx.getSocketFactory();
+  }
+
+
+  /**
+   * This returns the default SSL socket factory.
+   *
+   * @return  <code>SocketFactory</code>
+   */
+  public static SocketFactory getDefault()
+  {
+    final TLSSocketFactory sf = new TLSSocketFactory();
+    try {
+      sf.initialize();
+    } catch (GeneralSecurityException e) {
+      final Log logger = LogFactory.getLog(TLSSocketFactory.class);
+      if (logger.isErrorEnabled()) {
+        logger.error("Error initializing socket factory", e);
+      }
+    }
+    return sf;
+  }
+
+
+  /**
+   * Provides a descriptive string representation of this instance.
+   *
+   * @return  String of the form $Classname::factory=$factory.
+   */
+  @Override
+  public String toString()
+  {
+    return
+      String.format(
+        "%s@%d::sslContextInitializer=%s,factory=%s," +
+        "enabledCipherSuites=%s,enabledProtocols=%s",
+        this.getClass().getName(),
+        this.hashCode(),
+        this.getSSLContextInitializer(),
+        this.getFactory(),
+        this.getEnabledCipherSuites(),
+        this.getEnabledProtocols());
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/ThreadLocalTLSSocketFactory.java b/src/main/java/edu/vt/middleware/ldap/ssl/ThreadLocalTLSSocketFactory.java
new file mode 100644
index 0000000..ad7b7e5
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/ThreadLocalTLSSocketFactory.java
@@ -0,0 +1,143 @@
+/*
+  $Id: ThreadLocalTLSSocketFactory.java 2231 2012-02-02 15:46:27Z dfisher $
+
+  Copyright (C) 2003-2012 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2231 $
+  Updated: $Date: 2012-02-02 15:46:27 +0000 (Thu, 02 Feb 2012) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * TLSSocketFactory implementation that uses a thread local variable to store
+ * configuration. Useful for SSL configurations that can only retrieve the
+ * SSLSocketFactory from getDefault().
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2231 $ $Date: 2012-02-02 15:46:27 +0000 (Thu, 02 Feb 2012) $
+ */
+public class ThreadLocalTLSSocketFactory extends TLSSocketFactory
+{
+
+  /** Thread local instance of the ssl config. */
+  private static final ThreadLocalSslConfig THREAD_LOCAL_SSL_CONFIG =
+    new ThreadLocalSslConfig();
+
+
+  /** {@inheritDoc} */
+  @Override
+  public SSLContextInitializer getSSLContextInitializer()
+  {
+    return THREAD_LOCAL_SSL_CONFIG.get();
+  }
+
+
+  /** {@inheritDoc} */
+  @Override
+  public void setSSLContextInitializer(final SSLContextInitializer initializer)
+  {
+    THREAD_LOCAL_SSL_CONFIG.set(initializer);
+  }
+
+
+  /**
+   * This returns the default SSL socket factory.
+   *
+   * @return  socket factory
+   */
+  public static SocketFactory getDefault()
+  {
+    final ThreadLocalTLSSocketFactory sf = new ThreadLocalTLSSocketFactory();
+    if (sf.getSSLContextInitializer() == null) {
+      throw new NullPointerException(
+        "Thread local sslContextInitializer has not been set");
+    }
+    try {
+      sf.initialize();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalArgumentException(
+        "Error initializing socket factory", e);
+    }
+    return sf;
+  }
+
+
+  /**
+   * Returns an instance of this socket factory configured with a hostname
+   * verifying trust manager.
+   *
+   * @param  names  to use for hostname verification
+   *
+   * @return  socket factory
+   */
+  public static SSLSocketFactory getHostnameVerifierFactory(
+    final String[] names)
+  {
+    final ThreadLocalTLSSocketFactory sf = new ThreadLocalTLSSocketFactory();
+    final DefaultSSLContextInitializer ctxInit =
+      new DefaultSSLContextInitializer();
+    try {
+      final TrustManagerFactory tmf = TrustManagerFactory.getInstance(
+        TrustManagerFactory.getDefaultAlgorithm());
+      tmf.init((KeyStore) null);
+      final TrustManager[] tm = tmf.getTrustManagers();
+      final X509TrustManager[] aggregate =
+        new X509TrustManager[tm != null ? tm.length + 1 : 1];
+      if (tm != null) {
+        for (int i = 0; i < tm.length; i++) {
+          aggregate[i] = (X509TrustManager) tm[i];
+        }
+      }
+      aggregate[aggregate.length - 1] = new HostnameVerifyingTrustManager(
+        new DefaultHostnameVerifier(), names);
+      ctxInit.setTrustManagers(
+        new TrustManager[] {new AggregateTrustManager(aggregate)});
+      sf.setSSLContextInitializer(ctxInit);
+      sf.initialize();
+    } catch (GeneralSecurityException e) {
+      throw new IllegalArgumentException(e);
+    }
+    return sf;
+  }
+
+
+  /**
+   * Provides a descriptive string representation of this instance.
+   *
+   * @return  String of the form $Classname::factory=$factory.
+   */
+  @Override
+  public String toString()
+  {
+    return
+      String.format(
+        "%s@%d::sslContextInitializer=%s,factory=%s," +
+        "enabledCipherSuites=%s,enabledProtocols=%s",
+        this.getClass().getName(),
+        this.hashCode(),
+        this.getSSLContextInitializer(),
+        this.getFactory(),
+        this.getEnabledCipherSuites(),
+        this.getEnabledProtocols());
+  }
+
+
+  /**
+   * Thread local class for {@link SslConfig}.
+   */
+  private static class ThreadLocalSslConfig
+    extends ThreadLocal<SSLContextInitializer> {}
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/X509CertificateCredentialReader.java b/src/main/java/edu/vt/middleware/ldap/ssl/X509CertificateCredentialReader.java
new file mode 100644
index 0000000..45dbd22
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/X509CertificateCredentialReader.java
@@ -0,0 +1,41 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+/**
+ * Loads an X.509 certificate credential from a classpath, filepath, or stream
+ * resource. Supported certificate formats include: PEM, DER, and PKCS7.
+ *
+ * @author  Middleware Services
+ * @version  $Revision$
+ */
+public class X509CertificateCredentialReader
+  extends AbstractCredentialReader<X509Certificate>
+{
+
+  /** {@inheritDoc} */
+  public X509Certificate read(final InputStream is, final String... params)
+    throws IOException, GeneralSecurityException
+  {
+    final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+    return
+      (X509Certificate) cf.generateCertificate(this.getBufferedInputStream(is));
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/X509CertificatesCredentialReader.java b/src/main/java/edu/vt/middleware/ldap/ssl/X509CertificatesCredentialReader.java
new file mode 100644
index 0000000..a9be266
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/X509CertificatesCredentialReader.java
@@ -0,0 +1,51 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loads X.509 certificate credentials from a classpath, filepath, or stream
+ * resource. Supported certificate formats include: PEM, DER, and PKCS7.
+ *
+ * @author  Middleware Services
+ * @version  $Revision$
+ */
+public class X509CertificatesCredentialReader
+  extends AbstractCredentialReader<X509Certificate[]>
+{
+
+  /** {@inheritDoc} */
+  public X509Certificate[] read(final InputStream is, final String... params)
+    throws IOException, GeneralSecurityException
+  {
+    final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+    final List<X509Certificate> certList = new ArrayList<X509Certificate>();
+    final InputStream bufIs = this.getBufferedInputStream(is);
+    while (bufIs.available() > 0) {
+      final X509Certificate cert = (X509Certificate) cf.generateCertificate(
+        bufIs);
+      if (cert != null) {
+        certList.add(cert);
+      }
+    }
+    return certList.toArray(new X509Certificate[certList.size()]);
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/X509CredentialConfig.java b/src/main/java/edu/vt/middleware/ldap/ssl/X509CredentialConfig.java
new file mode 100644
index 0000000..95275b4
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/X509CredentialConfig.java
@@ -0,0 +1,140 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/**
+ * Provides the properties necessary for creating an SSL context initializer
+ * with a <code>X509CredentialReader</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public class X509CredentialConfig implements CredentialConfig
+{
+
+  /** Reads X.509 certificates credential. */
+  protected X509CertificatesCredentialReader certsReader =
+    new X509CertificatesCredentialReader();
+
+  /** Reads X.509 certificate credential. */
+  protected X509CertificateCredentialReader certReader =
+    new X509CertificateCredentialReader();
+
+  /** Reads private key credential. */
+  protected PrivateKeyCredentialReader keyReader =
+    new PrivateKeyCredentialReader();
+
+  /** Name of the trust certificates to use for the SSL connection. */
+  private String trustCertificates;
+
+  /** Name of the authentication certificate to use for the SSL connection. */
+  private String authenticationCertificate;
+
+  /** Name of the key to use for the SSL connection. */
+  private String authenticationKey;
+
+
+  /**
+   * This returns the name of the trust certificates to use.
+   *
+   * @return  <code>String</code> trust certificates name
+   */
+  public String getTrustCertificates()
+  {
+    return this.trustCertificates;
+  }
+
+
+  /**
+   * This sets the name of the trust certificates to use.
+   *
+   * @param  s  <code>String</code> trust certificates name
+   */
+  public void setTrustCertificates(final String s)
+  {
+    this.trustCertificates = s;
+  }
+
+
+  /**
+   * This returns the name of the authentication certificate to use.
+   *
+   * @return  <code>String</code> authentication certificate name
+   */
+  public String getAuthenticationCertificate()
+  {
+    return this.authenticationCertificate;
+  }
+
+
+  /**
+   * This sets the name of the authentication certificate to use.
+   *
+   * @param  s  <code>String</code> authentication certificate name
+   */
+  public void setAuthenticationCertificate(final String s)
+  {
+    this.authenticationCertificate = s;
+  }
+
+
+  /**
+   * This returns the name of the authentication key to use.
+   *
+   * @return  <code>String</code> authentication key name
+   */
+  public String getAuthenticationKey()
+  {
+    return this.authenticationKey;
+  }
+
+
+  /**
+   * This sets the name of the authentication key to use.
+   *
+   * @param  s  <code>String</code> authentication key name
+   */
+  public void setAuthenticationKey(final String s)
+  {
+    this.authenticationKey = s;
+  }
+
+
+  /** {@inheritDoc} */
+  public SSLContextInitializer createSSLContextInitializer()
+    throws GeneralSecurityException
+  {
+    final X509SSLContextInitializer sslInit = new X509SSLContextInitializer();
+    try {
+      if (this.trustCertificates != null) {
+        sslInit.setTrustCertificates(
+          this.certsReader.read(this.trustCertificates));
+      }
+      if (this.authenticationCertificate != null) {
+        sslInit.setAuthenticationCertificate(
+          this.certReader.read(this.authenticationCertificate));
+      }
+      if (this.authenticationKey != null) {
+        sslInit.setAuthenticationKey(
+          this.keyReader.read(this.authenticationKey));
+      }
+    } catch (IOException e) {
+      throw new GeneralSecurityException(e);
+    }
+    return sslInit;
+  }
+}
diff --git a/src/main/java/edu/vt/middleware/ldap/ssl/X509SSLContextInitializer.java b/src/main/java/edu/vt/middleware/ldap/ssl/X509SSLContextInitializer.java
new file mode 100644
index 0000000..d2c8c58
--- /dev/null
+++ b/src/main/java/edu/vt/middleware/ldap/ssl/X509SSLContextInitializer.java
@@ -0,0 +1,162 @@
+/*
+  $Id$
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision$
+  Updated: $Date$
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+/**
+ * Provides a <code>SSLContextInitializer</code> which can use X509 certificates
+ * to create key and trust managers.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1106 $ $Date: 2010-01-29 23:34:13 -0500 (Fri, 29 Jan 2010) $
+ */
+public class X509SSLContextInitializer extends AbstractSSLContextInitializer
+{
+
+  /** Certificates used to create trust managers. */
+  private X509Certificate[] trustCerts;
+
+  /** Certificate used to create key managers. */
+  private X509Certificate authenticationCert;
+
+  /** Private key used to create key managers. */
+  private PrivateKey authenticationKey;
+
+
+  /**
+   * Returns the certificates to use for creating the trust managers.
+   *
+   * @return  <code>X509Certificates[]</code>
+   */
+  public X509Certificate[] getTrustCertificates()
+  {
+    return this.trustCerts;
+  }
+
+
+  /**
+   * Sets the certificates to use for creating the trust managers.
+   *
+   * @param  certs  <code>X509Certificates[]</code>
+   */
+  public void setTrustCertificates(final X509Certificate[] certs)
+  {
+    this.trustCerts = certs;
+  }
+
+
+  /**
+   * Returns the certificate to use for creating the key managers.
+   *
+   * @return  <code>X509Certificate</code>
+   */
+  public X509Certificate getAuthenticationCertificate()
+  {
+    return this.authenticationCert;
+  }
+
+
+  /**
+   * Sets the certificate to use for creating the key managers.
+   *
+   * @param  cert  <code>X509Certificate</code>
+   */
+  public void setAuthenticationCertificate(final X509Certificate cert)
+  {
+    this.authenticationCert = cert;
+  }
+
+
+  /**
+   * Returns the private key associated with the authentication certificate.
+   *
+   * @return  <code>PrivateKey</code>
+   */
+  public PrivateKey getAuthenticationKey()
+  {
+    return this.authenticationKey;
+  }
+
+
+  /**
+   * Sets the private key associated with the authentication certificate.
+   *
+   * @param  key  <code>PrivateKey</code>
+   */
+  public void setAuthenticationKey(final PrivateKey key)
+  {
+    this.authenticationKey = key;
+  }
+
+
+  /** {@inheritDoc} */
+  public TrustManager[] getTrustManagers()
+    throws GeneralSecurityException
+  {
+    TrustManager[] tm = null;
+    if (this.trustCerts != null && this.trustCerts.length > 0) {
+      final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+      try {
+        ks.load(null, null);
+      } catch (IOException e) {
+        throw new GeneralSecurityException(e);
+      }
+      for (int i = 0; i < this.trustCerts.length; i++) {
+        ks.setCertificateEntry("ldap_trust_" + i, this.trustCerts[i]);
+      }
+
+      final TrustManagerFactory tmf = TrustManagerFactory.getInstance(
+        TrustManagerFactory.getDefaultAlgorithm());
+      tmf.init(ks);
+      tm = tmf.getTrustManagers();
+    }
+    return tm;
+  }
+
+
+  /** {@inheritDoc} */
+  public KeyManager[] getKeyManagers()
+    throws GeneralSecurityException
+  {
+    KeyManager[] km = null;
+    if (this.authenticationCert != null && this.authenticationKey != null) {
+      final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+      try {
+        ks.load(null, null);
+      } catch (IOException e) {
+        throw new GeneralSecurityException(e);
+      }
+      ks.setKeyEntry(
+        "ldap_client_auth",
+        this.authenticationKey,
+        "changeit".toCharArray(),
+        new X509Certificate[] {this.authenticationCert});
+
+      final KeyManagerFactory kmf = KeyManagerFactory.getInstance(
+        KeyManagerFactory.getDefaultAlgorithm());
+      kmf.init(ks, "changeit".toCharArray());
+      km = kmf.getKeyManagers();
+    }
+    return km;
+  }
+}
diff --git a/src/main/resources/edu/vt/middleware/ldap/LdapCli.args b/src/main/resources/edu/vt/middleware/ldap/LdapCli.args
new file mode 100644
index 0000000..55dbe7e
--- /dev/null
+++ b/src/main/resources/edu/vt/middleware/ldap/LdapCli.args
@@ -0,0 +1,24 @@
+authoritative:whether to require an authoritative source (true|false)
+authtype:type of authentication (none|simple|strong)
+baseDn:base dn
+batchSize:result batch size
+binaryAttributes:space delimited list of binary attributes
+bindCredential:password for the bindDn
+bindDn:fully qualified dn to bind as
+countLimit:maximum number of entries to return
+derefAliases:how to handle aliases (always|never|finding|searching)
+derefLinkFlag:whether to force dereferencing (true|false)
+handlerIgnoreExceptions:comma delimited list of exceptions that the handler should ignore
+hostnameVerifier:fully qualified class name which implements HostnameVerifier
+ignoreCase:whether to ignore case in attribute names (true|false)
+language:preferred language
+ldapUrl:URL that identifies the LDAP server to search
+referral:how to handle referrals (throw|ignore|follow)
+searchResultHandlers:comma delimited list of search result handlers to use
+searchScope:scope of the search (0:object|1:one level|2: subtree)
+ssl:whether to use ssl (true|false)
+sslSocketFactory:fully qualified class name which implements SSLSocketFactory
+timeout: amount of time in milliseconds that connect operations will block
+timeLimit:amount of time in milliseconds that search operations will block
+tls:whether to use tls (true|false)
+typesOnly:whether to return only attribute types (true|false)
diff --git a/src/main/resources/edu/vt/middleware/ldap/LdapCli.examples b/src/main/resources/edu/vt/middleware/ldap/LdapCli.examples
new file mode 100644
index 0000000..9bda9ce
--- /dev/null
+++ b/src/main/resources/edu/vt/middleware/ldap/LdapCli.examples
@@ -0,0 +1,19 @@
+- Search a ldap directory returning the mail attribute in ldif format
+
+   ldapsearch -ldapUrl ldap://directory.vt.edu -baseDn ou=People,dc=vt,dc=edu \
+              -query uupid=dfisher mail
+
+- Search a ldap directory returning all attributes in dsmlv1 format
+
+   ldapsearch -ldapUrl ldap://directory.vt.edu -baseDn ou=People,dc=vt,dc=edu \
+              -dsmlv1 -query uupid=dfisher
+
+- Search a ldap directory as an authenticated user returning all attributes in dsmlv2 format
+
+   ldapsearch -ldapUrl ldap://directory.vt.edu -baseDn ou=People,dc=vt,dc=edu \
+              -bindDn uid=818037,ou=People,dc=vt,dc=edu -tls true \
+              -dsmlv2 -query uupid=dfisher
+
+- Display all the command line options available 
+
+   ldapsearch -help
diff --git a/src/main/resources/edu/vt/middleware/ldap/auth/AuthenticatorCli.args b/src/main/resources/edu/vt/middleware/ldap/auth/AuthenticatorCli.args
new file mode 100644
index 0000000..f753e69
--- /dev/null
+++ b/src/main/resources/edu/vt/middleware/ldap/auth/AuthenticatorCli.args
@@ -0,0 +1,32 @@
+authoritative:whether to require an authoritative source (true|false)
+authtype:type of authentication (none|simple|strong)
+baseDn:base dn
+batchSize:result batch size
+binaryAttributes:space delimited list of binary attributes
+bindCredential:password for the bindDn
+bindDn:fully qualified dn to bind as
+countLimit:maximum number of entries to return
+derefAliases:how to handle aliases (always|never|finding|searching)
+derefLinkFlag:whether to force dereferencing (true|false)
+handlerIgnoreExceptions:comma delimited list of exceptions that the handler should ignore
+hostnameVerifier:fully qualified class name which implements HostnameVerifier
+ignoreCase:whether to ignore case in attribute names (true|false)
+language:preferred language
+ldapUrl:URL that identifies the LDAP server to search
+referral:how to handle referrals (throw|ignore|follow)
+searchResultHandlers:comma delimited list of search result handlers to use
+searchScope:scope of the search (0:object|1:one level|2: subtree)
+ssl:whether to use ssl (true|false)
+sslSocketFactory:fully qualified class name which implements SSLSocketFactory
+timeout: amount of time in milliseconds that connect operations will block
+timeLimit:amount of time in milliseconds that search operations will block
+tls:whether to use tls (true|false)
+typesOnly:whether to return only attribute types (true|false)
+authenticationResultHandlers:comma delimited list of authentication result handlers to use
+authorizationFilter:ldap filter to authorize user with
+constructDn:whether to construct the user dn instead of looking it up (true|false)
+credential:password to bind with
+subtreeSearch:whether to lookup user dn with a subtree search (true|false)
+user:combined with userField to lookup user dn
+userField:combined with user to lookup user dn
+userFilter:ldap filter to lookup user dn
diff --git a/src/main/resources/edu/vt/middleware/ldap/auth/AuthenticatorCli.examples b/src/main/resources/edu/vt/middleware/ldap/auth/AuthenticatorCli.examples
new file mode 100644
index 0000000..093d746
--- /dev/null
+++ b/src/main/resources/edu/vt/middleware/ldap/auth/AuthenticatorCli.examples
@@ -0,0 +1,18 @@
+- Authenticate a user and return their ldap entry
+
+   ldapauth -ldapUrl ldap://directory.vt.edu -baseDn ou=People,dc=vt,dc=edu \
+            -tls true -userField uupid
+
+- Authenticate and authorize a user on their accountState attribute
+
+   ldapauth -ldapUrl ldap://directory.vt.edu -baseDn ou=People,dc=vt,dc=edu \
+            -tls true -userField uupid -authorizationFilter accountState=active
+
+- Authenticate a user and return their mail attribute in dsmlv1 format
+
+   ldapauth -ldapUrl ldap://directory.vt.edu -baseDn ou=People,dc=vt,dc=edu \
+            -tls true -userField uupid -dsmlv1 mail
+
+- Display all the command line options available 
+
+   ldapauth -help
diff --git a/src/test/java/edu/vt/middleware/ldap/AnyHostnameVerifier.java b/src/test/java/edu/vt/middleware/ldap/AnyHostnameVerifier.java
new file mode 100644
index 0000000..d18ba00
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/AnyHostnameVerifier.java
@@ -0,0 +1,72 @@
+/*
+  $Id: AnyHostnameVerifier.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+/**
+ * <code>AnyHostnameVerifier</code> returns true for any host.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class AnyHostnameVerifier implements HostnameVerifier
+{
+
+
+  /** {@inheritDoc} */
+  public boolean verify(final String hostname, final SSLSession seession)
+  {
+    return true;
+  }
+
+
+  /**
+   * Dummy getter method.
+   *
+   * @return  'foo'
+   */
+  public String getFoo()
+  {
+    return "foo";
+  }
+
+
+  /**
+   * Dummy setter method. Noop.
+   *
+   * @param  s  <code>String</code>
+   */
+  public void setFoo(final String s) {}
+
+
+  /**
+   * Dummy getter method.
+   *
+   * @return  true
+   */
+  public boolean getBar()
+  {
+    return true;
+  }
+
+
+  /**
+   * Dummy setter method. Noop.
+   *
+   * @param  b  <code>boolean</code>
+   */
+  public void setBar(final boolean b) {}
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/LdapCliTest.java b/src/test/java/edu/vt/middleware/ldap/LdapCliTest.java
new file mode 100644
index 0000000..9226c39
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/LdapCliTest.java
@@ -0,0 +1,104 @@
+/*
+  $Id: LdapCliTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link LdapCli} class.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdapCliTest
+{
+
+  /** Entry created for ldap tests. */
+  private static LdapEntry testLdapEntry;
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry5" })
+  @BeforeClass(groups = {"ldapclitest"})
+  public void createLdapEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"ldapclitest"})
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  args  List of delimited arguments to pass to the CLI.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "cliSearchArgs", "cliSearchResults" })
+  @Test(groups = {"ldapclitest"})
+  public void search(final String args, final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    final PrintStream oldStdOut = System.out;
+    try {
+      final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+      System.setOut(new PrintStream(outStream));
+
+      LdapCli.main(args.split("\\|"));
+      AssertJUnit.assertEquals(
+        TestUtil.convertLdifToEntry(ldif),
+        TestUtil.convertLdifToEntry(outStream.toString()));
+    } finally {
+      // Restore STDOUT
+      System.setOut(oldStdOut);
+    }
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/LdapConfigTest.java b/src/test/java/edu/vt/middleware/ldap/LdapConfigTest.java
new file mode 100644
index 0000000..5c14b9f
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/LdapConfigTest.java
@@ -0,0 +1,92 @@
+/*
+  $Id: LdapConfigTest.java 1500 2010-08-18 15:01:02Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1500 $
+  Updated: $Date: 2010-08-18 16:01:02 +0100 (Wed, 18 Aug 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.util.Arrays;
+import edu.vt.middleware.ldap.handler.BinarySearchResultHandler;
+import edu.vt.middleware.ldap.handler.EntryDnSearchResultHandler;
+import edu.vt.middleware.ldap.handler.MergeSearchResultHandler;
+import edu.vt.middleware.ldap.handler.RecursiveSearchResultHandler;
+import edu.vt.middleware.ldap.handler.SearchResultHandler;
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link LdapConfig}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1500 $
+ */
+public class LdapConfigTest
+{
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"ldaptest"})
+  public void nullProperties()
+    throws Exception
+  {
+    final Ldap l = new Ldap();
+    l.loadFromProperties(
+      LdapConfigTest.class.getResourceAsStream("/ldap.null.properties"));
+
+    AssertJUnit.assertNull(l.getLdapConfig().getSslSocketFactory());
+    AssertJUnit.assertNull(l.getLdapConfig().getHostnameVerifier());
+    AssertJUnit.assertNull(l.getLdapConfig().getOperationRetryExceptions());
+    AssertJUnit.assertNull(l.getLdapConfig().getSearchResultHandlers());
+    AssertJUnit.assertNull(l.getLdapConfig().getHandlerIgnoreExceptions());
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"ldaptest"})
+  public void parserProperties()
+    throws Exception
+  {
+    final Ldap l = new Ldap();
+    l.loadFromProperties(
+      LdapConfigTest.class.getResourceAsStream("/ldap.parser.properties"));
+
+    AssertJUnit.assertEquals(
+      LdapConfig.SearchScope.OBJECT, l.getLdapConfig().getSearchScope());
+    AssertJUnit.assertEquals(10, l.getLdapConfig().getBatchSize());
+    AssertJUnit.assertEquals(5000, l.getLdapConfig().getTimeLimit());
+    AssertJUnit.assertEquals(8000, l.getLdapConfig().getTimeout());
+    AssertJUnit.assertEquals(
+      "jpegPhoto", l.getLdapConfig().getBinaryAttributes());
+
+    for (SearchResultHandler srh :
+         l.getLdapConfig().getSearchResultHandlers()) {
+      if (RecursiveSearchResultHandler.class.isInstance(srh)) {
+        final RecursiveSearchResultHandler h = (RecursiveSearchResultHandler)
+          srh;
+        AssertJUnit.assertEquals("member", h.getSearchAttribute());
+        AssertJUnit.assertEquals(
+          Arrays.asList(new String[] {"mail", "department"}),
+          Arrays.asList(h.getMergeAttributes()));
+      } else if (MergeSearchResultHandler.class.isInstance(srh)) {
+        final MergeSearchResultHandler h = (MergeSearchResultHandler) srh;
+        AssertJUnit.assertTrue(h.getAllowDuplicates());
+      } else if (BinarySearchResultHandler.class.isInstance(srh)) {
+        final BinarySearchResultHandler h = (BinarySearchResultHandler) srh;
+        AssertJUnit.assertNotNull(h);
+      } else if (EntryDnSearchResultHandler.class.isInstance(srh)) {
+        final EntryDnSearchResultHandler h = (EntryDnSearchResultHandler) srh;
+        AssertJUnit.assertEquals("myDN", h.getDnAttributeName());
+      } else {
+        throw new Exception("Unknown search result handler type " + srh);
+      }
+    }
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/LdapConnStrategyTest.java b/src/test/java/edu/vt/middleware/ldap/LdapConnStrategyTest.java
new file mode 100644
index 0000000..04a74d0
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/LdapConnStrategyTest.java
@@ -0,0 +1,70 @@
+/*
+  $Id: LdapConnStrategyTest.java 1442 2010-07-01 18:05:58Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1442 $
+  Updated: $Date: 2010-07-01 19:05:58 +0100 (Thu, 01 Jul 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link ConnectionHandler} with different strategies.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1442 $
+ */
+public class LdapConnStrategyTest
+{
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"ldaptest"})
+  public void connect()
+    throws Exception
+  {
+    final Ldap l = new Ldap();
+    l.loadFromProperties(
+      LdapConnStrategyTest.class.getResourceAsStream("/ldap.conn.properties"));
+
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+    AssertJUnit.assertTrue(l.connect());
+
+    l.getLdapConfig().getConnectionHandler().setConnectionStrategy(
+      ConnectionHandler.ConnectionStrategy.DEFAULT);
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+    AssertJUnit.assertTrue(l.connect());
+
+    l.getLdapConfig().getConnectionHandler().setConnectionStrategy(
+      ConnectionHandler.ConnectionStrategy.ACTIVE_PASSIVE);
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+    AssertJUnit.assertTrue(l.connect());
+
+    l.getLdapConfig().getConnectionHandler().setConnectionStrategy(
+      ConnectionHandler.ConnectionStrategy.RANDOM);
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+    AssertJUnit.assertTrue(l.connect());
+    l.close();
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/LdapConnTest.java b/src/test/java/edu/vt/middleware/ldap/LdapConnTest.java
new file mode 100644
index 0000000..446634b
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/LdapConnTest.java
@@ -0,0 +1,58 @@
+/*
+  $Id: LdapConnTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterSuite;
+import org.testng.annotations.Parameters;
+
+/**
+ * Sleeps at the end of all tests and check open connections.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdapConnTest
+{
+
+
+  /**
+   * @param  host  to check for connections with.
+   * @param  sleepTime  time to sleep for.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "ldapHost", "sleepTime" })
+  @AfterSuite(groups = {"conntest"})
+  public void sleep(final String host, final int sleepTime)
+    throws Exception
+  {
+    Thread.sleep(sleepTime);
+
+    /*
+     * -- expected open connections --
+     * LdapTest: 1
+     * LdapCliTest:0
+     * AuthenticatorTest: 2
+     * AuthenticatorCliTest: 0
+     * LdapResultTest: 0
+     * LdapLoginModuleTest: 0
+     * SessionManagerTest: 1
+     * SearchServletTest: 6
+     * AttributeServletTest: 3
+     */
+    final int openConns = TestUtil.countOpenConnections(host);
+    AssertJUnit.assertEquals(13, openConns);
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/LdapTest.java b/src/test/java/edu/vt/middleware/ldap/LdapTest.java
new file mode 100644
index 0000000..76de5b8
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/LdapTest.java
@@ -0,0 +1,1529 @@
+/*
+  $Id: LdapTest.java 1998 2011-06-20 17:56:35Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1998 $
+  Updated: $Date: 2011-06-20 18:56:35 +0100 (Mon, 20 Jun 2011) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.LimitExceededException;
+import javax.naming.NameClassPair;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+import javax.naming.SizeLimitExceededException;
+import javax.naming.TimeLimitExceededException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InvalidSearchFilterException;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.Ldap.AttributeModification;
+import edu.vt.middleware.ldap.bean.LdapAttribute;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import edu.vt.middleware.ldap.handler.AttributeHandler;
+import edu.vt.middleware.ldap.handler.BinaryAttributeHandler;
+import edu.vt.middleware.ldap.handler.BinarySearchResultHandler;
+import edu.vt.middleware.ldap.handler.CaseChangeSearchResultHandler;
+import edu.vt.middleware.ldap.handler.CaseChangeSearchResultHandler.CaseChange;
+import edu.vt.middleware.ldap.handler.EntryDnSearchResultHandler;
+import edu.vt.middleware.ldap.handler.FqdnSearchResultHandler;
+import edu.vt.middleware.ldap.handler.MergeSearchResultHandler;
+import edu.vt.middleware.ldap.handler.RecursiveAttributeHandler;
+import edu.vt.middleware.ldap.handler.RecursiveSearchResultHandler;
+import edu.vt.middleware.ldap.handler.SearchResultHandler;
+import edu.vt.middleware.ldap.ldif.Ldif;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link Ldap}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1998 $
+ */
+public class LdapTest
+{
+
+  /** Invalid search filter. */
+  public static final String INVALID_FILTER = "(cn=not-a-name)";
+
+  /** Entry created for ldap tests. */
+  private static LdapEntry testLdapEntry;
+
+  /** Entry created for ldap tests. */
+  private static LdapEntry specialCharsLdapEntry;
+
+  /** Entries for group tests. */
+  private static Map<String, LdapEntry[]> groupEntries =
+    new HashMap<String, LdapEntry[]>();
+
+  /**
+   * Initialize the map of group entries.
+   */
+  static {
+    for (int i = 2; i <= 5; i++) {
+      groupEntries.put(String.valueOf(i), new LdapEntry[2]);
+    }
+  }
+
+  /** Ldap instance for concurrency testing. */
+  private Ldap singleLdap;
+
+
+  /**
+   * Default constructor.
+   *
+   * @throws  Exception  if ldap cannot be constructed
+   */
+  public LdapTest()
+    throws Exception
+  {
+    this.singleLdap = TestUtil.createLdap();
+  }
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry2" })
+  @BeforeClass(groups = {"ldaptest"})
+  public void createLdapEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /**
+   * @param  ldifFile2  to create.
+   * @param  ldifFile3  to create.
+   * @param  ldifFile4  to create.
+   * @param  ldifFile5  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "createGroup2",
+      "createGroup3",
+      "createGroup4",
+      "createGroup5"
+    }
+  )
+  @BeforeClass(groups = {"ldaptest"})
+  public void createGroupEntry(
+    final String ldifFile2,
+    final String ldifFile3,
+    final String ldifFile4,
+    final String ldifFile5)
+    throws Exception
+  {
+    groupEntries.get("2")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile2));
+    groupEntries.get("3")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile3));
+    groupEntries.get("4")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile4));
+    groupEntries.get("5")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile5));
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    for (Map.Entry<String, LdapEntry[]> e : groupEntries.entrySet()) {
+      ldap.create(
+        e.getValue()[0].getDn(),
+        e.getValue()[0].getLdapAttributes().toAttributes());
+    }
+    ldap.close();
+
+    ldap = TestUtil.createLdap();
+    for (Map.Entry<String, LdapEntry[]> e : groupEntries.entrySet()) {
+      while (
+        !ldap.compare(
+            e.getValue()[0].getDn(),
+            new SearchFilter(e.getValue()[0].getDn().split(",")[0]))) {
+        Thread.sleep(100);
+      }
+    }
+
+    // setup group relationships
+    ldap.modifyAttributes(
+      groupEntries.get("2")[0].getDn(),
+      AttributeModification.ADD,
+      AttributesFactory.createAttributes(
+        "member",
+        "uugid=group3,ou=test,dc=vt,dc=edu"));
+    ldap.modifyAttributes(
+      groupEntries.get("3")[0].getDn(),
+      AttributeModification.ADD,
+      AttributesFactory.createAttributes(
+        "member",
+        new String[] {
+          "uugid=group4,ou=test,dc=vt,dc=edu",
+          "uugid=group5,ou=test,dc=vt,dc=edu",
+        }));
+    ldap.modifyAttributes(
+      groupEntries.get("4")[0].getDn(),
+      AttributeModification.ADD,
+      AttributesFactory.createAttributes(
+        "member",
+        "uugid=group3,ou=test,dc=vt,dc=edu"));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createSpecialCharsEntry" })
+  @BeforeClass(groups = {"ldaptest"})
+  public void createSpecialCharsEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    specialCharsLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      specialCharsLdapEntry.getDn(),
+      specialCharsLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+        specialCharsLdapEntry.getDn(),
+          new SearchFilter(specialCharsLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /**
+   * @param  oldDn  to rename.
+   * @param  newDn  to rename to.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "renameOldDn", "renameNewDn" })
+  @AfterClass(groups = {"ldaptest"})
+  public void renameLdapEntry(final String oldDn, final String newDn)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+    AssertJUnit.assertNotNull(ldap.getAttributes(oldDn));
+    ldap.rename(oldDn, newDn);
+    AssertJUnit.assertNotNull(ldap.getAttributes(newDn));
+    try {
+      ldap.getAttributes(oldDn);
+      AssertJUnit.fail(
+        "Should have thrown NameNotFoundException, no exception thrown");
+    } catch (NameNotFoundException e) {
+      AssertJUnit.assertEquals(NameNotFoundException.class, e.getClass());
+    } catch (Exception e) {
+      AssertJUnit.fail("Should have thrown NameNotFoundException, threw " + e);
+    }
+    ldap.rename(newDn, oldDn);
+    AssertJUnit.assertNotNull(ldap.getAttributes(oldDn));
+    try {
+      ldap.getAttributes(newDn);
+      AssertJUnit.fail(
+        "Should have thrown NameNotFoundException, no exception thrown");
+    } catch (NameNotFoundException e) {
+      AssertJUnit.assertEquals(NameNotFoundException.class, e.getClass());
+    } catch (Exception e) {
+      AssertJUnit.fail("Should have thrown NameNotFoundException, threw " + e);
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(
+    groups = {"ldaptest"},
+    dependsOnMethods = {"renameLdapEntry"}
+  )
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.delete(specialCharsLdapEntry.getDn());
+    ldap.delete(groupEntries.get("2")[0].getDn());
+    ldap.delete(groupEntries.get("3")[0].getDn());
+    ldap.delete(groupEntries.get("4")[0].getDn());
+    ldap.delete(groupEntries.get("5")[0].getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  createNew  whether to construct a new ldap instance.
+   *
+   * @return  <code>Ldap</code>
+   *
+   * @throws  Exception  On ldap construction failure.
+   */
+  public Ldap createLdap(final boolean createNew)
+    throws Exception
+  {
+    if (createNew) {
+      return TestUtil.createLdap();
+    }
+    return singleLdap;
+  }
+
+
+  /**
+   * @param  dn  to compare.
+   * @param  filter  to compare with.
+   * @param  filterArgs  to replace args in filter with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "compareDn", "compareFilter", "compareFilterArgs" })
+  @Test(
+    groups = {"ldaptest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void compare(
+    final String dn,
+    final String filter,
+    final String filterArgs)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(false);
+    AssertJUnit.assertFalse(
+      ldap.compare(dn, INVALID_FILTER, filterArgs.split("\\|")));
+    AssertJUnit.assertTrue(ldap.compare(dn, filter, filterArgs.split("\\|")));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  filterArgs  to replace args in filter with.
+   * @param  returnAttrs  to return from search.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "searchDn",
+      "searchFilter",
+      "searchFilterArgs",
+      "searchReturnAttrs",
+      "searchResults"
+    }
+  )
+  @Test(
+    groups = {"ldaptest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void search(
+    final String dn,
+    final String filter,
+    final String filterArgs,
+    final String returnAttrs,
+    final String ldifFile)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(false);
+
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final LdapEntry entry = TestUtil.convertLdifToEntry(expected);
+    final LdapEntry shortDnEntry = TestUtil.convertLdifToEntry(expected);
+    shortDnEntry.setDn(
+      shortDnEntry.getDn().substring(0, shortDnEntry.getDn().indexOf(",")));
+
+    final LdapEntry entryDnEntry = TestUtil.convertLdifToEntry(expected);
+    entryDnEntry.getLdapAttributes().addAttribute(
+      "entryDN",
+      entryDnEntry.getDn());
+
+    // test searching
+    Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"));
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+
+    // test searching without handler
+    iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"),
+      new SearchResultHandler[0]);
+    AssertJUnit.assertEquals(
+      shortDnEntry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+
+    // test searching with multiple handlers
+    final EntryDnSearchResultHandler srh = new EntryDnSearchResultHandler();
+    iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"),
+      new FqdnSearchResultHandler(),
+      srh);
+    AssertJUnit.assertEquals(
+      entryDnEntry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+
+    // test that entry dn handler is no-op if attribute name conflicts
+    srh.setDnAttributeName("givenName");
+    iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"),
+      new FqdnSearchResultHandler(),
+      srh);
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "pagedSearchDn",
+      "pagedSearchFilter",
+      "pagedSearchResults"
+    })
+  @Test(groups = {"ldaptest"})
+  public void pagedSearch(
+    final String dn,
+    final String filter,
+    final String ldifFile)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+    ldap.getLdapConfig().setPagedResultsSize(1);
+
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final LdapResult result = TestUtil.convertLdifToResult(expected);
+
+    // test searching
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter));
+    AssertJUnit.assertEquals(
+      result,
+      TestUtil.convertLdifToResult((new Ldif()).createLdif(iter)));
+
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  filterArgs  to replace args in filter with.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "recursiveSearchDn",
+      "recursiveSearchFilter",
+      "recursiveSearchFilterArgs",
+      "recursiveAttributeHandlerResults"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void recursiveAttributeHandlerSearch(
+    final String dn,
+    final String filter,
+    final String filterArgs,
+    final String ldifFile)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(false);
+
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final LdapEntry entry = TestUtil.convertLdifToEntry(expected);
+
+    // test recursive searching
+    final FqdnSearchResultHandler handler = new FqdnSearchResultHandler();
+    handler.setAttributeHandler(
+      new AttributeHandler[] {new RecursiveAttributeHandler("member")});
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      (String[]) null,
+      handler);
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  filterArgs  to replace args in filter with.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "recursiveSearchDn",
+      "recursiveSearchFilter",
+      "recursiveSearchFilterArgs",
+      "recursiveSearchResultHandlerResults"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void recursiveSearchResultHandlerSearch(
+    final String dn,
+    final String filter,
+    final String filterArgs,
+    final String ldifFile)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(false);
+
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final LdapEntry entry = TestUtil.convertLdifToEntry(expected);
+
+    // test recursive searching
+    final FqdnSearchResultHandler fsrh = new FqdnSearchResultHandler();
+    final RecursiveSearchResultHandler rsrh = new RecursiveSearchResultHandler(
+      "member",
+      new String[] {"uugid", "uid"});
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      (String[]) null,
+      fsrh,
+      rsrh);
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "mergeSearchDn",
+      "mergeSearchFilter",
+      "mergeSearchResults"
+    })
+  @Test(groups = {"ldaptest"})
+  public void mergeSearch(
+    final String dn,
+    final String filter,
+    final String ldifFile)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(false);
+
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final LdapEntry entry = TestUtil.convertLdifToEntry(expected);
+
+    // test merge searching
+    final MergeSearchResultHandler handler = new MergeSearchResultHandler();
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter),
+      (String[]) null,
+      new FqdnSearchResultHandler(),
+      handler);
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "mergeDuplicateSearchDn",
+      "mergeDuplicateSearchFilter",
+      "mergeDuplicateSearchResults"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void mergeDuplicateSearch(
+    final String dn,
+    final String filter,
+    final String ldifFile)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(false);
+
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final LdapEntry entry = TestUtil.convertLdifToEntry(expected);
+
+    // test merge searching
+    final MergeSearchResultHandler handler = new MergeSearchResultHandler();
+    handler.setAllowDuplicates(true);
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter),
+      (String[]) null,
+      new FqdnSearchResultHandler(),
+      handler);
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  returnAttr  to return from search.
+   * @param  base64Value  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "binarySearchDn",
+      "binarySearchFilter",
+      "binarySearchReturnAttr",
+      "binarySearchResult"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void binarySearch(
+    final String dn,
+    final String filter,
+    final String returnAttr,
+    final String base64Value)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(false);
+
+    // test binary searching
+    Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter),
+      new String[] {returnAttr},
+      new FqdnSearchResultHandler());
+    AssertJUnit.assertNotSame(
+      base64Value,
+      iter.next().getAttributes().get(returnAttr).get());
+
+    iter = ldap.search(
+      dn,
+      new SearchFilter(filter),
+      new String[] {returnAttr},
+      new FqdnSearchResultHandler(),
+      new BinarySearchResultHandler());
+    AssertJUnit.assertEquals(
+      base64Value,
+      iter.next().getAttributes().get(returnAttr).get());
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  filterArgs  to replace args in filter with.
+   * @param  returnAttrs  to return from search.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+      {
+        "searchDn",
+        "searchFilter",
+        "searchFilterArgs",
+        "searchReturnAttrs",
+        "searchResults"
+      }
+    )
+  @Test(groups = {"ldaptest"})
+  public void caseChangeSearch(
+    final String dn,
+    final String filter,
+    final String filterArgs,
+    final String returnAttrs,
+    final String ldifFile)
+    throws Exception
+  {
+    final CaseChangeSearchResultHandler srh =
+      new CaseChangeSearchResultHandler();
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final Ldap ldap = this.createLdap(true);
+
+    // test no case change
+    final LdapEntry noChangeEntry = TestUtil.convertLdifToEntry(expected);
+    Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"),
+      new FqdnSearchResultHandler(),
+      srh);
+    AssertJUnit.assertEquals(
+      noChangeEntry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+
+    // test lower case attribute values
+    srh.setAttributeValueCaseChange(CaseChange.LOWER);
+    final LdapEntry lcValuesChangeEntry = TestUtil.convertLdifToEntry(expected);
+    for (LdapAttribute la :
+         lcValuesChangeEntry.getLdapAttributes().getAttributes()) {
+      final Set<Object> s = new HashSet<Object>();
+      for (Object o : la.getValues()) {
+        if (o instanceof String) {
+          s.add(((String) o).toLowerCase());
+        }
+      }
+      la.getValues().clear();
+      la.getValues().addAll(s);
+    }
+    iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"),
+      new FqdnSearchResultHandler(),
+      srh);
+    AssertJUnit.assertEquals(
+      lcValuesChangeEntry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+
+    // test upper case attribute names
+    srh.setAttributeValueCaseChange(CaseChange.NONE);
+    srh.setAttributeNameCaseChange(CaseChange.UPPER);
+    final LdapEntry ucNamesChangeEntry = TestUtil.convertLdifToEntry(expected);
+    for (LdapAttribute la :
+         ucNamesChangeEntry.getLdapAttributes().getAttributes()) {
+      la.setName(la.getName().toUpperCase());
+    }
+    iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"),
+      new FqdnSearchResultHandler(),
+      srh);
+    AssertJUnit.assertEquals(
+      ucNamesChangeEntry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+
+    // test lower case everything
+    srh.setAttributeValueCaseChange(CaseChange.LOWER);
+    srh.setAttributeNameCaseChange(CaseChange.LOWER);
+    srh.setDnCaseChange(CaseChange.LOWER);
+    final LdapEntry lcAllChangeEntry = TestUtil.convertLdifToEntry(expected);
+    for (LdapAttribute la :
+         ucNamesChangeEntry.getLdapAttributes().getAttributes()) {
+      lcAllChangeEntry.setDn(lcAllChangeEntry.getDn().toLowerCase());
+      la.setName(la.getName().toLowerCase());
+      final Set<Object> s = new HashSet<Object>();
+      for (Object o : la.getValues()) {
+        if (o instanceof String) {
+          s.add(((String) o).toLowerCase());
+        }
+      }
+      la.getValues().clear();
+      la.getValues().addAll(s);
+    }
+    iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"),
+      new FqdnSearchResultHandler(),
+      srh);
+    AssertJUnit.assertEquals(
+      ucNamesChangeEntry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  resultsSize  of search results.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "searchExceptionDn",
+      "searchExceptionFilter",
+      "searchExceptionResultsSize"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void searchWithException(
+    final String dn,
+    final String filter,
+    final int resultsSize)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+
+    // test exception searching
+    ldap.getLdapConfig().setCountLimit(resultsSize);
+    ldap.getLdapConfig().setHandlerIgnoreExceptions(null);
+
+    try {
+      ldap.search(dn, new SearchFilter("(uugid=*)"));
+      AssertJUnit.fail("Should have thrown SizeLimitExceededException");
+    } catch (NamingException e) {
+      AssertJUnit.assertEquals(SizeLimitExceededException.class, e.getClass());
+    }
+
+    ldap.getLdapConfig().setHandlerIgnoreExceptions(
+      new Class[] {TimeLimitExceededException.class});
+    try {
+      ldap.search(dn, new SearchFilter("(uugid=*)"));
+      AssertJUnit.fail("Should have thrown SizeLimitExceededException");
+    } catch (NamingException e) {
+      AssertJUnit.assertEquals(SizeLimitExceededException.class, e.getClass());
+    }
+
+    ldap.getLdapConfig().setHandlerIgnoreExceptions(
+      new Class[] {LimitExceededException.class});
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter));
+    AssertJUnit.assertEquals(resultsSize, TestUtil.newLdapResult(iter).size());
+
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"ldaptest"})
+  public void searchWithRetry()
+    throws Exception
+  {
+    final RetryLdap ldap = new RetryLdap();
+    ldap.setLdapConfig(this.createLdap(true).getLdapConfig());
+    ldap.getLdapConfig().setOperationRetryExceptions(
+      new Class[] {InvalidSearchFilterException.class});
+
+    // test defaults
+    try {
+      ldap.search(new SearchFilter("(("));
+    } catch (InvalidSearchFilterException e) {
+      AssertJUnit.assertEquals(
+        InvalidSearchFilterException.class,
+        e.getClass());
+    }
+    AssertJUnit.assertEquals(1, ldap.getRetryCount());
+    AssertJUnit.assertEquals(
+      ldap.getRunTime(),
+      Math.min(ldap.getRunTime(), 50));
+
+    // test no retry
+    ldap.reset();
+    ldap.getLdapConfig().setOperationRetry(0);
+
+    try {
+      ldap.search(new SearchFilter("(("));
+    } catch (InvalidSearchFilterException e) {
+      AssertJUnit.assertEquals(
+        InvalidSearchFilterException.class,
+        e.getClass());
+    }
+    AssertJUnit.assertEquals(0, ldap.getRetryCount());
+    AssertJUnit.assertEquals(0, ldap.getRunTime());
+
+    // test no exception
+    ldap.reset();
+    ldap.getLdapConfig().setOperationRetry(1);
+    ldap.getLdapConfig().setOperationRetryExceptions(null);
+
+    try {
+      ldap.search(new SearchFilter("(("));
+    } catch (InvalidSearchFilterException e) {
+      AssertJUnit.assertEquals(
+        InvalidSearchFilterException.class,
+        e.getClass());
+    }
+    AssertJUnit.assertEquals(0, ldap.getRetryCount());
+    AssertJUnit.assertEquals(0, ldap.getRunTime());
+
+    // test retry count and wait time
+    ldap.reset();
+    ldap.getLdapConfig().setOperationRetry(3);
+    ldap.getLdapConfig().setOperationRetryWait(1000);
+    ldap.getLdapConfig().setOperationRetryExceptions(
+      new Class[] {InvalidSearchFilterException.class});
+
+    try {
+      ldap.search(new SearchFilter("(("));
+    } catch (InvalidSearchFilterException e) {
+      AssertJUnit.assertEquals(
+        InvalidSearchFilterException.class,
+        e.getClass());
+    }
+    AssertJUnit.assertEquals(3, ldap.getRetryCount());
+    AssertJUnit.assertTrue(ldap.getRunTime() % 3000 < 30);
+
+    // test backoff interval
+    ldap.reset();
+    ldap.getLdapConfig().setOperationRetryBackoff(2);
+    try {
+      ldap.search(new SearchFilter("(("));
+    } catch (InvalidSearchFilterException e) {
+      AssertJUnit.assertEquals(
+        InvalidSearchFilterException.class,
+        e.getClass());
+    }
+    AssertJUnit.assertEquals(3, ldap.getRetryCount());
+    AssertJUnit.assertTrue(ldap.getRunTime() % 7000 < 70);
+
+    // test infinite retries
+    ldap.reset();
+    ldap.setStopCount(10);
+    ldap.getLdapConfig().setOperationRetry(-1);
+    try {
+      ldap.search(new SearchFilter("(("));
+    } catch (InvalidSearchFilterException e) {
+      AssertJUnit.assertEquals(
+        InvalidSearchFilterException.class,
+        e.getClass());
+    }
+    AssertJUnit.assertEquals(10, ldap.getRetryCount());
+    AssertJUnit.assertTrue(ldap.getRunTime() % 111000 < 111);
+
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  returnAttrs  to return from search.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "searchAttributesDn",
+      "searchAttributesFilter",
+      "searchAttributesReturnAttrs",
+      "searchAttributesResults"
+    }
+  )
+  @Test(
+    groups = {"ldaptest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void searchAttributes(
+    final String dn,
+    final String filter,
+    final String returnAttrs,
+    final String ldifFile)
+    throws Exception
+  {
+    final String[] matchAttrs = filter.split("=");
+    final Ldap ldap = this.createLdap(false);
+    // test searching
+    Iterator<SearchResult> iter = ldap.searchAttributes(
+      dn,
+      AttributesFactory.createAttributes(matchAttrs[0], matchAttrs[1]),
+      returnAttrs.split("\\|"));
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    AssertJUnit.assertEquals(
+      TestUtil.convertLdifToEntry(expected),
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+    // test searching without handler
+    iter = ldap.searchAttributes(
+      dn,
+      AttributesFactory.createAttributes(matchAttrs[0], matchAttrs[1]),
+      returnAttrs.split("\\|"),
+      new SearchResultHandler[0]);
+
+    final LdapEntry entry = TestUtil.convertLdifToEntry(expected);
+    entry.setDn(entry.getDn().substring(0, entry.getDn().indexOf(",")));
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "specialCharSearchDn",
+      "specialCharSearchFilter",
+      "specialCharSearchResults"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void searchSpecialChars(
+    final String dn,
+    final String filter,
+    final String ldifFile)
+    throws Exception
+  {
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final LdapEntry entry = TestUtil.convertLdifToEntry(expected);
+    // only remove escaped '/'
+    entry.setDn(entry.getDn().replaceAll("\\\\/", "/"));
+
+    final Ldap ldap = this.createLdap(false);
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn, new SearchFilter(filter));
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "rewriteSearchDn",
+      "rewriteSearchFilter",
+      "rewriteSearchResults"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void searchRewrite(
+    final String dn,
+    final String filter,
+    final String ldifFile)
+    throws Exception
+  {
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    final LdapEntry entry = TestUtil.convertLdifToEntry(expected);
+    // remove all escaped characters
+    entry.setDn(entry.getDn().replaceAll("\\\\", ""));
+
+    final Ldap ldap = this.createLdap(false);
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn, new SearchFilter(filter));
+    AssertJUnit.assertEquals(
+      entry,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  results  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "listDn", "listResults" })
+  @Test(groups = {"ldaptest"})
+  public void list(final String dn, final String results)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+    final Iterator<NameClassPair> iter = ldap.list(dn);
+    final List<String> l = new ArrayList<String>();
+    while (iter.hasNext()) {
+      final NameClassPair ncp = iter.next();
+      l.add(ncp.getName());
+    }
+
+    final List<String> expected = Arrays.asList(results.split("\\|"));
+    AssertJUnit.assertTrue(l.containsAll(expected));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  results  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "listBindingsDn", "listBindingsResults" })
+  @Test(groups = {"ldaptest"})
+  public void listBindings(final String dn, final String results)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+    final Iterator<Binding> iter = ldap.listBindings(dn);
+    final List<String> l = new ArrayList<String>();
+    while (iter.hasNext()) {
+      final Binding b = iter.next();
+      if (Context.class.isAssignableFrom(b.getObject().getClass())) {
+        ((Context) b.getObject()).close();
+      }
+      l.add(b.getName());
+    }
+
+    final List<String> expected = Arrays.asList(results.split("\\|"));
+    AssertJUnit.assertTrue(l.containsAll(expected));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  returnAttrs  to return from search.
+   * @param  results  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "getAttributesDn",
+      "getAttributesReturnAttrs",
+      "getAttributesResults"
+    }
+  )
+  @Test(
+    groups = {"ldaptest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void getAttributes(
+    final String dn,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(false);
+    final Attributes attrs = ldap.getAttributes(dn, returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  returnAttrs  to return from search.
+   * @param  results  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "getAttributesBase64Dn",
+      "getAttributesBase64ReturnAttrs",
+      "getAttributesBase64Results"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void getAttributesBase64(
+    final String dn,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+    final Attributes attrs = ldap.getAttributes(
+      dn,
+      returnAttrs.split("\\|"),
+      new BinaryAttributeHandler());
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "getSchemaDn", "getSchemaResults" })
+  @Test(groups = {"ldaptest"})
+  public void getSchema(final String dn, final String ldifFile)
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+    final Iterator<SearchResult> iter = ldap.getSchema(dn);
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    AssertJUnit.assertEquals(
+      TestUtil.convertLdifToResult(expected),
+      TestUtil.convertLdifToResult((new Ldif()).createLdif(iter)));
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(
+    groups = {"ldaptest"},
+    enabled = false
+  )
+  public void getSaslMechanisms()
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+    ldap.getSaslMechanisms();
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(
+    groups = {"ldaptest"},
+    enabled = false
+  )
+  public void getSupportedControls()
+    throws Exception
+  {
+    final Ldap ldap = this.createLdap(true);
+    ldap.getSupportedControls();
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to modify.
+   * @param  attrs  to add.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "addAttributeDn", "addAttributeAttribute" })
+  @Test(groups = {"ldaptest"})
+  public void addAttribute(final String dn, final String attrs)
+    throws Exception
+  {
+    final LdapAttribute expected = TestUtil.convertStringToAttributes(attrs)
+        .getAttributes().iterator().next();
+    final Ldap ldap = this.createLdap(true);
+    ldap.modifyAttributes(
+      dn,
+      AttributeModification.ADD,
+      AttributesFactory.createAttributes(
+        expected.getName(),
+        expected.getValues().toArray()));
+
+    final Attributes a = ldap.getAttributes(
+      dn,
+      new String[] {expected.getName()});
+    AssertJUnit.assertEquals(
+      expected,
+      TestUtil.newLdapAttributes(a).getAttribute(expected.getName()));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to modify.
+   * @param  attrs  to add.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "addAttributesDn", "addAttributesAttributes" })
+  @Test(groups = {"ldaptest"})
+  public void addAttributes(final String dn, final String attrs)
+    throws Exception
+  {
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(attrs);
+    final Ldap ldap = this.createLdap(true);
+    final ModificationItem[] mods = new ModificationItem[expected.size()];
+    int i = 0;
+    for (LdapAttribute la : expected.getAttributes()) {
+      mods[i] = new ModificationItem(
+        DirContext.ADD_ATTRIBUTE,
+        la.toAttribute());
+      i++;
+    }
+    ldap.modifyAttributes(dn, mods);
+
+    final Attributes a = ldap.getAttributes(dn, expected.getAttributeNames());
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(a));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to modify.
+   * @param  attrs  to replace.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "replaceAttributeDn", "replaceAttributeAttribute" })
+  @Test(
+    groups = {"ldaptest"},
+    dependsOnMethods = {"addAttribute"}
+  )
+  public void replaceAttribute(final String dn, final String attrs)
+    throws Exception
+  {
+    final LdapAttribute expected = TestUtil.convertStringToAttributes(attrs)
+        .getAttributes().iterator().next();
+    final Ldap ldap = this.createLdap(true);
+    ldap.modifyAttributes(
+      dn,
+      AttributeModification.REPLACE,
+      AttributesFactory.createAttributes(
+        expected.getName(),
+        expected.getValues().toArray()));
+
+    final Attributes a = ldap.getAttributes(
+      dn,
+      new String[] {expected.getName()});
+    AssertJUnit.assertEquals(
+      expected,
+      TestUtil.newLdapAttributes(a).getAttribute(expected.getName()));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to modify.
+   * @param  attrs  to replace.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "replaceAttributesDn", "replaceAttributesAttributes" })
+  @Test(
+    groups = {"ldaptest"},
+    dependsOnMethods = {"addAttributes"}
+  )
+  public void replaceAttributes(final String dn, final String attrs)
+    throws Exception
+  {
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(attrs);
+    final Ldap ldap = this.createLdap(true);
+    final ModificationItem[] mods = new ModificationItem[expected.size()];
+    int i = 0;
+    for (LdapAttribute la : expected.getAttributes()) {
+      mods[i] = new ModificationItem(
+        DirContext.REPLACE_ATTRIBUTE,
+        la.toAttribute());
+      i++;
+    }
+    ldap.modifyAttributes(dn, mods);
+
+    final Attributes a = ldap.getAttributes(dn, expected.getAttributeNames());
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(a));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to modify.
+   * @param  attrs  to remove.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "removeAttributeDn", "removeAttributeAttribute" })
+  @Test(
+    groups = {"ldaptest"},
+    dependsOnMethods = {"replaceAttribute"}
+  )
+  public void removeAttribute(final String dn, final String attrs)
+    throws Exception
+  {
+    final LdapAttribute expected = TestUtil.convertStringToAttributes(attrs)
+        .getAttributes().iterator().next();
+    final LdapAttribute remove = TestUtil.convertStringToAttributes(attrs)
+        .getAttributes().iterator().next();
+    remove.getValues().remove("Unit Test User");
+    expected.getValues().remove("Best Test User");
+
+    final Ldap ldap = this.createLdap(true);
+    ldap.modifyAttributes(
+      dn,
+      AttributeModification.REMOVE,
+      AttributesFactory.createAttributes(
+        remove.getName(),
+        remove.getValues().toArray()));
+
+    final Attributes a = ldap.getAttributes(
+      dn,
+      new String[] {expected.getName()});
+    AssertJUnit.assertEquals(
+      expected,
+      TestUtil.newLdapAttributes(a).getAttribute(expected.getName()));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to modify.
+   * @param  attrs  to remove.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "removeAttributesDn", "removeAttributesAttributes" })
+  @Test(
+    groups = {"ldaptest"},
+    dependsOnMethods = {"replaceAttributes"}
+  )
+  public void removeAttributes(final String dn, final String attrs)
+    throws Exception
+  {
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(attrs);
+    final LdapAttributes remove = TestUtil.convertStringToAttributes(attrs);
+
+    final String[] attrsName = remove.getAttributeNames();
+    remove.getAttributes().remove(remove.getAttribute(attrsName[0]));
+    expected.getAttributes().remove(expected.getAttribute(attrsName[1]));
+
+    final Ldap ldap = this.createLdap(true);
+    final ModificationItem[] mods = new ModificationItem[expected.size()];
+    int i = 0;
+    for (LdapAttribute la : remove.getAttributes()) {
+      mods[i] = new ModificationItem(
+        DirContext.REMOVE_ATTRIBUTE,
+        la.toAttribute());
+      i++;
+    }
+    ldap.modifyAttributes(dn, mods);
+
+    final Attributes a = ldap.getAttributes(dn, expected.getAttributeNames());
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(a));
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"ldaptest"})
+  public void saslExternalConnect()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSaslExternalLdap();
+    AssertJUnit.assertTrue(ldap.connect());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  krb5Realm  kerberos realm
+   * @param  krb5Kdc  kerberos kdc
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   * @param  filterArgs  to replace args in filter with.
+   * @param  returnAttrs  to return from search.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "krb5Realm",
+      "krb5Kdc",
+      "gssApiSearchDn",
+      "gssApiSearchFilter",
+      "gssApiSearchFilterArgs",
+      "gssApiSearchReturnAttrs",
+      "gssApiSearchResults"
+    }
+  )
+  @Test(groups = {"ldaptest"})
+  public void gssApiSearch(
+    final String krb5Realm,
+    final String krb5Kdc,
+    final String dn,
+    final String filter,
+    final String filterArgs,
+    final String returnAttrs,
+    final String ldifFile)
+    throws Exception
+  {
+    System.setProperty(
+      "java.security.auth.login.config",
+      "src/test/resources/ldap_jaas.config");
+    System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
+    System.setProperty("java.security.krb5.realm", krb5Realm);
+    System.setProperty("java.security.krb5.kdc", krb5Kdc);
+
+    final Ldap ldap = TestUtil.createGssApiLdap();
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"));
+    final String expected = TestUtil.readFileIntoString(ldifFile);
+    AssertJUnit.assertEquals(
+      TestUtil.convertLdifToEntry(expected),
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+    ldap.close();
+
+    System.clearProperty("java.security.auth.login.config");
+    System.clearProperty("javax.security.auth.useSubjectCredsOnly");
+    System.clearProperty("java.security.krb5.realm");
+    System.clearProperty("java.security.krb5.kdc");
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/RetryLdap.java b/src/test/java/edu/vt/middleware/ldap/RetryLdap.java
new file mode 100644
index 0000000..00db6b0
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/RetryLdap.java
@@ -0,0 +1,100 @@
+/*
+  $Id: RetryLdap.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+
+/**
+ * <code>RetyLdap</code> provides a wrapper class for testing {@link
+ * #operationRetry()}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class RetryLdap extends Ldap
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = 4247614583961731974L;
+
+  /** retry counter. */
+  private int retryCount;
+
+  /** run time counter. */
+  private long runTime;
+
+  /** stop counter. */
+  private int stopCount;
+
+
+  /**
+   * Returns the retry count.
+   *
+   * @return  retry count
+   */
+  public int getRetryCount()
+  {
+    return this.retryCount;
+  }
+
+
+  /**
+   * Returns the run time counter.
+   *
+   * @return  run time
+   */
+  public long getRunTime()
+  {
+    return this.runTime;
+  }
+
+
+  /**
+   * Sets the count at which to stop retries.
+   *
+   * @param  i  stop count
+   */
+  public void setStopCount(final int i)
+  {
+    this.stopCount = i;
+  }
+
+
+  /** Resets all the counters. */
+  public void reset()
+  {
+    this.retryCount = 0;
+    this.runTime = 0;
+    this.stopCount = 0;
+  }
+
+
+  /** {@inheritDoc} */
+  protected void operationRetry(
+    final LdapContext ctx,
+    final NamingException e,
+    final int count)
+    throws NamingException
+  {
+    this.retryCount = count;
+
+    final long t = System.currentTimeMillis();
+    super.operationRetry(ctx, e, count);
+    this.runTime += System.currentTimeMillis() - t;
+    if (this.stopCount > 0 && this.retryCount == this.stopCount) {
+      throw e;
+    }
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/SpringTest.java b/src/test/java/edu/vt/middleware/ldap/SpringTest.java
new file mode 100644
index 0000000..e26e0da
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/SpringTest.java
@@ -0,0 +1,58 @@
+/*
+  $Id: SpringTest.java 1500 2010-08-18 15:01:02Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1500 $
+  Updated: $Date: 2010-08-18 16:01:02 +0100 (Wed, 18 Aug 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import edu.vt.middleware.ldap.pool.BlockingLdapPool;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for Spring integration.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1500 $
+ */
+public class SpringTest
+{
+
+
+  /**
+   * Attempts to load all Spring application context XML files to
+   * verify proper wiring.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(groups = {"ldaptest"})
+  public void testSpringWiring()
+    throws Exception
+  {
+    final ClassPathXmlApplicationContext context =
+      new ClassPathXmlApplicationContext(new String[] {
+        "/spring-context.xml",
+      });
+    AssertJUnit.assertTrue(context.getBeanDefinitionCount() > 0);
+    final Ldap l = (Ldap) context.getBean("ldap");
+    l.close();
+
+    final ClassPathXmlApplicationContext poolContext =
+      new ClassPathXmlApplicationContext(new String[] {
+        "/spring-pool-context.xml",
+      });
+    AssertJUnit.assertTrue(poolContext.getBeanDefinitionCount() > 0);
+    final BlockingLdapPool lp =
+      (BlockingLdapPool) poolContext.getBean("ldapPool");
+    lp.close();
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/TestUtil.java b/src/test/java/edu/vt/middleware/ldap/TestUtil.java
new file mode 100644
index 0000000..4c9cef8
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/TestUtil.java
@@ -0,0 +1,409 @@
+/*
+  $Id: TestUtil.java 1876 2011-04-05 14:36:44Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1876 $
+  Updated: $Date: 2011-04-05 15:36:44 +0100 (Tue, 05 Apr 2011) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.auth.Authenticator;
+import edu.vt.middleware.ldap.auth.NoopDnResolver;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapBeanProvider;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import edu.vt.middleware.ldap.ldif.Ldif;
+import org.testng.annotations.DataProvider;
+
+/**
+ * Common methods for ldap tests.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1876 $
+ */
+public final class TestUtil
+{
+
+  /** Location of the hostname in the output of netstat. */
+  public static final int NETSTAT_HOST_INDEX = 4;
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "setup-ldap")
+  public static Ldap createSetupLdap()
+    throws Exception
+  {
+    final Ldap l = new Ldap();
+    l.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.setup.properties"));
+    return l;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "ldap")
+  public static Ldap createLdap()
+    throws Exception
+  {
+    final Ldap l = new Ldap();
+    l.loadFromProperties();
+    return l;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "sasl-external-ldap")
+  public static Ldap createSaslExternalLdap()
+    throws Exception
+  {
+    final Ldap l = new Ldap();
+    l.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.sasl.properties"));
+    return l;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "gss-api-ldap")
+  public static Ldap createGssApiLdap()
+    throws Exception
+  {
+    final Ldap l = new Ldap();
+    l.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.gssapi.properties"));
+    return l;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "ssl-auth")
+  public static Authenticator createSSLAuthenticator()
+    throws Exception
+  {
+    final Authenticator a = new Authenticator();
+    a.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.ssl.properties"));
+    return a;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "ssl-dn-auth")
+  public static Authenticator createSSLDnAuthenticator()
+    throws Exception
+  {
+    final Authenticator a = new Authenticator();
+    a.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.ssl.properties"));
+    a.getAuthenticatorConfig().setDnResolver(new NoopDnResolver());
+    return a;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "tls-auth")
+  public static Authenticator createTLSAuthenticator()
+    throws Exception
+  {
+    final Authenticator a = new Authenticator();
+    a.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.tls.properties"));
+    return a;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "tls-dn-auth")
+  public static Authenticator createTLSDnAuthenticator()
+    throws Exception
+  {
+    final Authenticator a = new Authenticator();
+    a.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.tls.properties"));
+    a.getAuthenticatorConfig().setDnResolver(new NoopDnResolver());
+    return a;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "digest-md5-auth")
+  public static Authenticator createDigestMD5Authenticator()
+    throws Exception
+  {
+    final Authenticator a = new Authenticator();
+    a.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.digest-md5.properties"));
+    a.getAuthenticatorConfig().setDnResolver(new NoopDnResolver());
+    return a;
+  }
+
+
+  /**
+   * @return  Test configuration.
+   *
+   * @throws  Exception  On test data generation failure.
+   */
+  @DataProvider(name = "cram-md5-auth")
+  public static Authenticator createCramMD5Authenticator()
+    throws Exception
+  {
+    final Authenticator a = new Authenticator();
+    a.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.cram-md5.properties"));
+    a.getAuthenticatorConfig().setDnResolver(new NoopDnResolver());
+    return a;
+  }
+
+
+  /**
+   * Reads a file on the classpath into a reader.
+   *
+   * @param  filename  to open.
+   *
+   * @return  reader.
+   *
+   * @throws  Exception  If file cannot be read.
+   */
+  public static BufferedReader readFile(final String filename)
+    throws Exception
+  {
+    return
+      new BufferedReader(
+        new InputStreamReader(TestUtil.class.getResourceAsStream(filename)));
+  }
+
+
+  /**
+   * Reads a file on the classpath into a string.
+   *
+   * @param  filename  to open.
+   *
+   * @return  string.
+   *
+   * @throws  Exception  If file cannot be read.
+   */
+  public static String readFileIntoString(final String filename)
+    throws Exception
+  {
+    final StringBuffer result = new StringBuffer();
+    final BufferedReader br = readFile(filename);
+    try {
+      String line;
+      while ((line = br.readLine()) != null) {
+        result.append(line).append(System.getProperty("line.separator"));
+      }
+    } finally {
+      br.close();
+    }
+    return result.toString();
+  }
+
+
+  /**
+   * Creates a new <code>LdapResult</code> with the supplied <code>
+   * Iterator</code> of search results.
+   *
+   * @param  iter  <code>Iterator</code> of search results
+   *
+   * @return  <code>LdapResult</code>
+   *
+   * @throws  Exception  if search results cannot be read
+   */
+  public static LdapResult newLdapResult(final Iterator<SearchResult> iter)
+    throws Exception
+  {
+    final LdapResult lr = LdapBeanProvider.getLdapBeanFactory().newLdapResult();
+    lr.addEntries(iter);
+    return lr;
+  }
+
+
+  /**
+   * Converts a ldif to a <code>LdapResult</code>.
+   *
+   * @param  ldif  to convert.
+   *
+   * @return  LdapResult.
+   *
+   * @throws  Exception  if ldif cannot be read
+   */
+  public static LdapResult convertLdifToResult(final String ldif)
+    throws Exception
+  {
+    return (new Ldif()).importLdifToLdapResult(new StringReader(ldif));
+  }
+
+
+  /**
+   * Creates a new <code>LdapEntry</code> with the supplied <code>
+   * SearchResult</code>.
+   *
+   * @param  sr  <code>SearchResult</code>
+   *
+   * @return  <code>LdapEntry</code>
+   *
+   * @throws  Exception  if search result cannot be read
+   */
+  public static LdapEntry newLdapEntry(final SearchResult sr)
+    throws Exception
+  {
+    final LdapEntry le = LdapBeanProvider.getLdapBeanFactory().newLdapEntry();
+    le.setEntry(sr);
+    return le;
+  }
+
+
+  /**
+   * Converts a ldif to a <code>LdapEntry</code>.
+   *
+   * @param  ldif  to convert.
+   *
+   * @return  LdapEntry.
+   *
+   * @throws  Exception  if ldif cannot be read
+   */
+  public static LdapEntry convertLdifToEntry(final String ldif)
+    throws Exception
+  {
+    final LdapResult lr =
+      (new Ldif()).importLdifToLdapResult(new StringReader(ldif));
+    if (lr.size() == 1) {
+      return lr.getEntries().iterator().next();
+    } else {
+      return null;
+    }
+  }
+
+
+  /**
+   * Creates a new <code>LdapAttributes</code> with the supplied <code>
+   * Attributes</code>.
+   *
+   * @param  attrs  <code>Attributes</code>
+   *
+   * @return  <code>LdapAttributes</code>
+   *
+   * @throws  Exception  if attributes cannot be read
+   */
+  public static LdapAttributes newLdapAttributes(final Attributes attrs)
+    throws Exception
+  {
+    final LdapAttributes la = LdapBeanProvider.getLdapBeanFactory()
+        .newLdapAttributes();
+    la.addAttributes(attrs);
+    return la;
+  }
+
+
+  /**
+   * Converts a string of the form: givenName=John|sn=Doe into a ldap attributes
+   * object.
+   *
+   * @param  attrs  to convert.
+   *
+   * @return  LdapAttributes.
+   */
+  public static LdapAttributes convertStringToAttributes(final String attrs)
+  {
+    final LdapAttributes la = LdapBeanProvider.getLdapBeanFactory()
+        .newLdapAttributes();
+    final String[] s = attrs.split("\\|");
+    for (int i = 0; i < s.length; i++) {
+      final String[] nameValuePairs = s[i].trim().split("=", 2);
+      if (la.getAttribute(nameValuePairs[0]) != null) {
+        la.getAttribute(nameValuePairs[0]).getValues().add(nameValuePairs[1]);
+      } else {
+        la.addAttribute(nameValuePairs[0], nameValuePairs[1]);
+      }
+    }
+    return la;
+  }
+
+
+  /**
+   * Returns the number of open connections to the supplied host. Uses 'netstat
+   * -al' to uncover open sockets.
+   *
+   * @param  host  host to look for.
+   *
+   * @return  number of open connections.
+   *
+   * @throws  IOException  if the process cannot be run
+   */
+  public static int countOpenConnections(final String host)
+    throws IOException
+  {
+    final String[] cmd = new String[] {"netstat", "-al"};
+    final Process p = new ProcessBuilder(cmd).start();
+    final BufferedReader br = new BufferedReader(
+      new InputStreamReader(p.getInputStream()));
+    String line;
+    final List<String> openConns = new ArrayList<String>();
+    while ((line = br.readLine()) != null) {
+      if (line.matches(".*ESTABLISHED$")) {
+        final String s = line.split("\\s+")[NETSTAT_HOST_INDEX];
+        openConns.add(s.substring(0, s.lastIndexOf(".")));
+      }
+    }
+
+    int count = 0;
+    for (String o : openConns) {
+      if (o.contains(host)) {
+        count++;
+      }
+    }
+    return count;
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/WikiCodeTest.java b/src/test/java/edu/vt/middleware/ldap/WikiCodeTest.java
new file mode 100644
index 0000000..329835a
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/WikiCodeTest.java
@@ -0,0 +1,490 @@
+/*
+  $Id: WikiCodeTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap;
+
+import java.io.BufferedWriter;
+import java.io.OutputStreamWriter;
+import java.util.Iterator;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.auth.Authenticator;
+import edu.vt.middleware.ldap.auth.AuthenticatorConfig;
+import edu.vt.middleware.ldap.dsml.Dsmlv1;
+import edu.vt.middleware.ldap.handler.AttributeHandler;
+import edu.vt.middleware.ldap.handler.BinaryAttributeHandler;
+import edu.vt.middleware.ldap.handler.EntryDnSearchResultHandler;
+import edu.vt.middleware.ldap.handler.FqdnSearchResultHandler;
+import edu.vt.middleware.ldap.handler.SearchResultHandler;
+import edu.vt.middleware.ldap.ldif.Ldif;
+import edu.vt.middleware.ldap.pool.BlockingLdapPool;
+import edu.vt.middleware.ldap.pool.BlockingTimeoutException;
+import edu.vt.middleware.ldap.pool.CloseLdapPassivator;
+import edu.vt.middleware.ldap.pool.CompareLdapValidator;
+import edu.vt.middleware.ldap.pool.ConnectLdapActivator;
+import edu.vt.middleware.ldap.pool.DefaultLdapFactory;
+import edu.vt.middleware.ldap.pool.LdapActivationException;
+import edu.vt.middleware.ldap.pool.LdapPoolConfig;
+import edu.vt.middleware.ldap.pool.LdapPoolException;
+import edu.vt.middleware.ldap.pool.LdapValidationException;
+import edu.vt.middleware.ldap.pool.PoolInterruptedException;
+import edu.vt.middleware.ldap.pool.SharedLdapPool;
+import edu.vt.middleware.ldap.pool.SoftLimitLdapPool;
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for wiki sample code.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class WikiCodeTest
+{
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleCompare()
+    throws Exception
+  {
+    final Ldap ldap = new Ldap(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    if (
+      ldap.compare(
+          "uid=818037,ou=People,dc=vt,dc=edu",
+          new SearchFilter("mail=dfisher at vt.edu"))) {
+      System.out.println("Compare succeeded");
+    } else {
+      System.out.println("Compare failed");
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleSubtreeSearch()
+    throws Exception
+  {
+    final Ldap ldap = new Ldap(
+      new LdapConfig("ldap://directory.vt.edu/dc=vt,dc=edu"));
+    (new Ldif()).outputLdif(
+      ldap.search(new SearchFilter("sn=Fisher")),
+      new BufferedWriter(new OutputStreamWriter(System.out)));
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleAttributeSearch()
+    throws Exception
+  {
+    final Ldap ldap = new Ldap(
+      new LdapConfig("ldap://directory.vt.edu", "ou=People,dc=vt,dc=edu"));
+    (new Dsmlv1()).outputDsml(
+      ldap.searchAttributes(
+        AttributesFactory.createAttributes("mail", "dfisher at vt.edu"),
+        new String[] {"sn", "givenName"}),
+      new BufferedWriter(new OutputStreamWriter(System.out)));
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleAuthentication()
+    throws Exception
+  {
+    final AuthenticatorConfig config = new AuthenticatorConfig(
+      "ldap://authn.directory.vt.edu",
+      "ou=People,dc=vt,dc=edu");
+    config.setTls(true);
+    // attribute to search for user with
+    config.setUserField(new String[] {"uid", "mail"});
+
+    final Authenticator auth = new Authenticator(config);
+    if (auth.authenticate("user", "credential")) {
+      System.out.println("Authentication succeeded");
+    } else {
+      System.out.println("Authentication failed");
+    }
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleAuthorization()
+    throws Exception
+  {
+    final AuthenticatorConfig config = new AuthenticatorConfig(
+      "ldap://authn.directory.vt.edu",
+      "ou=People,dc=vt,dc=edu");
+    config.setTls(true);
+    // attribute to search for user with
+    config.setUserFilter("(|(uid={0})(mail={0}))");
+
+    final Authenticator auth = new Authenticator(config);
+    if (
+      auth.authenticate(
+          "user",
+          "credential",
+          new SearchFilter("eduPersonAffiliation=staff"))) {
+      System.out.println("Authentication/Authorization succeeded");
+    } else {
+      System.out.println("Authentication/Authorization failed");
+    }
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void samplePooling()
+    throws Exception
+  {
+    final DefaultLdapFactory factory = new DefaultLdapFactory(
+      new LdapConfig("ldap://directory.vt.edu/ou=People,dc=vt,dc=edu"));
+    final SoftLimitLdapPool pool = new SoftLimitLdapPool(factory);
+    pool.initialize();
+
+    Ldap ldap = null;
+    try {
+      ldap = pool.checkOut();
+
+      final Iterator<SearchResult> i = ldap.search(
+        new SearchFilter("givenName=Daniel"),
+        new String[] {"uid", "mail"});
+
+      AssertJUnit.assertTrue(i.hasNext());
+
+    } catch (LdapPoolException e) {
+      e.printStackTrace();
+    } finally {
+      pool.checkIn(ldap);
+    }
+
+    pool.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleNoHandler()
+    throws Exception
+  {
+    final Ldap ldap = new Ldap(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    final Iterator<SearchResult> iter = ldap.search(
+      "ou=People,dc=vt,dc=edu",
+      new SearchFilter("sn=Fisher"),
+      new String[] {"givenName", "mail"},
+      (SearchResultHandler[]) null);
+
+    AssertJUnit.assertTrue(iter.hasNext());
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleBinaryAttributeHandler()
+    throws Exception
+  {
+    final Ldap ldap = new Ldap(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    final Attributes attrs = ldap.getAttributes(
+      "uid=818037,ou=People,dc=vt,dc=edu",
+      null,
+      new BinaryAttributeHandler());
+
+    AssertJUnit.assertTrue(attrs.size() > 0);
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleSearchBinaryAttributeHandler()
+    throws Exception
+  {
+    final Ldap ldap = new Ldap(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    final FqdnSearchResultHandler handler = new FqdnSearchResultHandler();
+    handler.setAttributeHandler(
+      new AttributeHandler[] {new BinaryAttributeHandler()});
+
+    final Iterator<SearchResult> iter = ldap.search(
+      "ou=People,dc=vt,dc=edu",
+      new SearchFilter("sn=Fisher"),
+      (String[]) null,
+      handler);
+
+    AssertJUnit.assertTrue(iter.hasNext());
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleEntryDnHandler()
+    throws Exception
+  {
+    final LdapConfig config = new LdapConfig(
+      "ldap://directory.vt.edu:389",
+      "ou=People,dc=vt,dc=edu");
+    config.setSearchResultHandlers(
+      new SearchResultHandler[] {
+        new FqdnSearchResultHandler(),
+        new EntryDnSearchResultHandler(),
+      });
+
+    final Ldap ldap = new Ldap(config);
+    final Iterator<SearchResult> iter = ldap.search(
+      new SearchFilter("sn=Fisher"));
+
+    AssertJUnit.assertTrue(iter.hasNext());
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleBlockingLdapPool()
+    throws Exception
+  {
+    final DefaultLdapFactory factory = new DefaultLdapFactory(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    final BlockingLdapPool pool = new BlockingLdapPool(factory);
+    // wait for 5sec for an object to be available
+    pool.setBlockWaitTime(5000);
+    pool.initialize();
+
+    Ldap ldap = null;
+    try {
+      ldap = pool.checkOut();
+
+      final Iterator<SearchResult> i = ldap.search(
+        new SearchFilter("givenName=Daniel"),
+        new String[] {"uid", "mail"});
+
+      AssertJUnit.assertTrue(i.hasNext());
+
+    } catch (BlockingTimeoutException e) {
+      e.printStackTrace();
+    } catch (PoolInterruptedException e) {
+      e.printStackTrace();
+    } catch (LdapPoolException e) {
+      e.printStackTrace();
+    } finally {
+      pool.checkIn(ldap);
+    }
+
+    pool.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleSoftLimitLdapPool()
+    throws Exception
+  {
+    final DefaultLdapFactory factory = new DefaultLdapFactory(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    final SoftLimitLdapPool pool = new SoftLimitLdapPool(factory);
+    // wait for 5sec for an object to be available
+    pool.setBlockWaitTime(5000);
+    pool.initialize();
+
+    Ldap ldap = null;
+    try {
+      ldap = pool.checkOut();
+
+      final Iterator<SearchResult> i = ldap.search(
+        new SearchFilter("givenName=Daniel"),
+        new String[] {"uid", "mail"});
+
+      AssertJUnit.assertTrue(i.hasNext());
+
+    } catch (BlockingTimeoutException e) {
+      e.printStackTrace();
+    } catch (PoolInterruptedException e) {
+      e.printStackTrace();
+    } catch (LdapPoolException e) {
+      e.printStackTrace();
+    } finally {
+      pool.checkIn(ldap);
+    }
+
+    pool.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleSharedLdapPool()
+    throws Exception
+  {
+    final DefaultLdapFactory factory = new DefaultLdapFactory(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    final SharedLdapPool pool = new SharedLdapPool(factory);
+    pool.initialize();
+
+    Ldap ldap = null;
+    try {
+      ldap = pool.checkOut();
+
+      final Iterator<SearchResult> i = ldap.search(
+        new SearchFilter("givenName=Daniel"),
+        new String[] {"uid", "mail"});
+
+      AssertJUnit.assertTrue(i.hasNext());
+
+    } catch (LdapPoolException e) {
+      e.printStackTrace();
+    } finally {
+      pool.checkIn(ldap);
+    }
+
+    pool.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleActivatePassivatePool()
+    throws Exception
+  {
+    final DefaultLdapFactory factory = new DefaultLdapFactory(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    factory.setConnectOnCreate(false);
+    factory.setLdapActivator(new ConnectLdapActivator());
+    factory.setLdapPassivator(new CloseLdapPassivator());
+
+    final SoftLimitLdapPool pool = new SoftLimitLdapPool(factory);
+    // wait for 5sec for an object to be available
+    pool.setBlockWaitTime(5000);
+    pool.initialize();
+
+    Ldap ldap = null;
+    try {
+      ldap = pool.checkOut();
+
+      final Iterator<SearchResult> i = ldap.search(
+        new SearchFilter("givenName=Daniel"),
+        new String[] {"uid", "mail"});
+
+      AssertJUnit.assertTrue(i.hasNext());
+
+    } catch (LdapActivationException e) {
+      e.printStackTrace();
+    } catch (BlockingTimeoutException e) {
+      e.printStackTrace();
+    } catch (PoolInterruptedException e) {
+      e.printStackTrace();
+    } catch (LdapPoolException e) {
+      e.printStackTrace();
+    } finally {
+      pool.checkIn(ldap);
+    }
+
+    pool.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void sampleValidatePool()
+    throws Exception
+  {
+    final LdapPoolConfig config = new LdapPoolConfig();
+    config.setValidateOnCheckOut(true);
+
+    // perform a simple compare
+    final DefaultLdapFactory factory = new DefaultLdapFactory(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    factory.setLdapValidator(
+      new CompareLdapValidator(
+        "ou=People,dc=vt,dc=edu",
+        new SearchFilter("ou=People")));
+
+    final SoftLimitLdapPool pool = new SoftLimitLdapPool(config, factory);
+    // wait for 5sec for an object to be available
+    pool.setBlockWaitTime(5000);
+    pool.initialize();
+
+    Ldap ldap = null;
+    try {
+      ldap = pool.checkOut();
+
+      final Iterator<SearchResult> i = ldap.search(
+        new SearchFilter("givenName=Daniel"),
+        new String[] {"uid", "mail"});
+
+      AssertJUnit.assertTrue(i.hasNext());
+
+    } catch (LdapValidationException e) {
+      e.printStackTrace();
+    } catch (BlockingTimeoutException e) {
+      e.printStackTrace();
+    } catch (PoolInterruptedException e) {
+      e.printStackTrace();
+    } catch (LdapPoolException e) {
+      e.printStackTrace();
+    } finally {
+      pool.checkIn(ldap);
+    }
+
+    pool.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"wikitest"})
+  public void samplePeriodicValidatePool()
+    throws Exception
+  {
+    final LdapPoolConfig config = new LdapPoolConfig();
+    // by default validate the pool every 30 min, if idle
+    config.setValidatePeriodically(true);
+
+    final DefaultLdapFactory factory = new DefaultLdapFactory(
+      new LdapConfig("ldap://directory.vt.edu:389", "ou=People,dc=vt,dc=edu"));
+    // perform a simple compare
+    factory.setLdapValidator(
+      new CompareLdapValidator(
+        "ou=People,dc=vt,dc=edu",
+        new SearchFilter("ou=People")));
+
+    final SoftLimitLdapPool pool = new SoftLimitLdapPool(config, factory);
+    pool.initialize();
+
+    Ldap ldap = null;
+    try {
+      ldap = pool.checkOut();
+
+      final Iterator<SearchResult> i = ldap.search(
+        new SearchFilter("givenName=Daniel"),
+        new String[] {"uid", "mail"});
+
+      AssertJUnit.assertTrue(i.hasNext());
+
+    } catch (LdapPoolException e) {
+      e.printStackTrace();
+    } finally {
+      pool.checkIn(ldap);
+    }
+
+    pool.close();
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorCliTest.java b/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorCliTest.java
new file mode 100644
index 0000000..9d83ae2
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorCliTest.java
@@ -0,0 +1,117 @@
+/*
+  $Id: AuthenticatorCliTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link AuthenticatorCli} class.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class AuthenticatorCliTest
+{
+
+  /** Entry created for ldap tests. */
+  private static LdapEntry testLdapEntry;
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry6" })
+  @BeforeClass(groups = {"authclitest"})
+  public void createLdapEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"authclitest"})
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  args  List of delimited arguments to pass to the CLI.
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "cliAuthArgs", "cliAuthResults" })
+  @Test(groups = {"authclitest"})
+  public void authenticate(final String args, final String ldifFile)
+    throws Exception
+  {
+    System.setProperty(
+      "javax.net.ssl.trustStore",
+      "src/test/resources/ed.truststore");
+    System.setProperty("javax.net.ssl.trustStoreType", "BKS");
+    System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
+
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    final PrintStream oldStdOut = System.out;
+    try {
+      final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+      System.setOut(new PrintStream(outStream));
+
+      AuthenticatorCli.main(args.split("\\|"));
+      AssertJUnit.assertEquals(
+        TestUtil.convertLdifToEntry(ldif),
+        TestUtil.convertLdifToEntry(outStream.toString()));
+    } finally {
+      // Restore STDOUT
+      System.setOut(oldStdOut);
+    }
+
+    System.clearProperty("javax.net.ssl.trustStore");
+    System.clearProperty("javax.net.ssl.trustStoreType");
+    System.clearProperty("javax.net.ssl.trustStorePassword");
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorLoadTest.java b/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorLoadTest.java
new file mode 100644
index 0000000..b42ea8e
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorLoadTest.java
@@ -0,0 +1,297 @@
+/*
+  $Id: AuthenticatorLoadTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.naming.directory.Attributes;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Load test for {@link Authenticator}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class AuthenticatorLoadTest
+{
+
+  /** Invalid password test data. */
+  public static final String INVALID_PASSWD = "not-a-password";
+
+  /** Invalid filter test data. */
+  public static final String INVALID_FILTER = "departmentNumber=1111";
+
+  /** Entries for auth tests. */
+  private static Map<String, LdapEntry[]> entries =
+    new HashMap<String, LdapEntry[]>();
+
+  /**
+   * Initialize the map of entries.
+   */
+  static {
+    for (int i = 2; i <= 10; i++) {
+      entries.put(String.valueOf(i), new LdapEntry[2]);
+    }
+  }
+
+  /** Ldap instance for concurrency testing. */
+  private Authenticator singleTLSAuth;
+
+
+  /**
+   * Default constructor.
+   *
+   * @throws  Exception  On test failure.
+   */
+  public AuthenticatorLoadTest()
+    throws Exception
+  {
+    this.singleTLSAuth = new Authenticator();
+    this.singleTLSAuth.loadFromProperties(
+      AuthenticatorLoadTest.class.getResourceAsStream(
+        "/ldap.tls.load.properties"));
+  }
+
+
+  /**
+   * @param  ldifFile2  to create.
+   * @param  ldifFile3  to create.
+   * @param  ldifFile4  to create.
+   * @param  ldifFile5  to create.
+   * @param  ldifFile6  to create.
+   * @param  ldifFile7  to create.
+   * @param  ldifFile8  to create.
+   * @param  ldifFile9  to create.
+   * @param  ldifFile10  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "createEntry2",
+      "createEntry3",
+      "createEntry4",
+      "createEntry5",
+      "createEntry6",
+      "createEntry7",
+      "createEntry8",
+      "createEntry9",
+      "createEntry10"
+    }
+  )
+  @BeforeClass(groups = {"authloadtest"})
+  public void createAuthEntry(
+    final String ldifFile2,
+    final String ldifFile3,
+    final String ldifFile4,
+    final String ldifFile5,
+    final String ldifFile6,
+    final String ldifFile7,
+    final String ldifFile8,
+    final String ldifFile9,
+    final String ldifFile10)
+    throws Exception
+  {
+    entries.get("2")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile2));
+    entries.get("3")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile3));
+    entries.get("4")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile4));
+    entries.get("5")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile5));
+    entries.get("6")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile6));
+    entries.get("7")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile7));
+    entries.get("8")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile8));
+    entries.get("9")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile9));
+    entries.get("10")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile10));
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    for (Map.Entry<String, LdapEntry[]> e : entries.entrySet()) {
+      ldap.create(
+        e.getValue()[0].getDn(),
+        e.getValue()[0].getLdapAttributes().toAttributes());
+    }
+    ldap.close();
+
+    ldap = TestUtil.createLdap();
+    for (Map.Entry<String, LdapEntry[]> e : entries.entrySet()) {
+      while (
+        !ldap.compare(
+            e.getValue()[0].getDn(),
+            new SearchFilter(e.getValue()[0].getDn().split(",")[0]))) {
+        Thread.sleep(100);
+      }
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"authloadtest"})
+  public void deleteAuthEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(entries.get("2")[0].getDn());
+    ldap.delete(entries.get("3")[0].getDn());
+    ldap.delete(entries.get("4")[0].getDn());
+    ldap.delete(entries.get("5")[0].getDn());
+    ldap.delete(entries.get("6")[0].getDn());
+    ldap.delete(entries.get("7")[0].getDn());
+    ldap.delete(entries.get("8")[0].getDn());
+    ldap.delete(entries.get("9")[0].getDn());
+    ldap.delete(entries.get("10")[0].getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * Sample authentication data.
+   *
+   * @return  user authentication data
+   */
+  @DataProvider(name = "auth-data")
+  public Object[][] createAuthData()
+  {
+    return
+      new Object[][] {
+        {
+          "jdoe2 at vt.edu",
+          "password2",
+          "departmentNumber={1}",
+          "0822",
+          "givenName|sn",
+          "givenName=John|sn=Doe",
+        },
+        {
+          "jdoe3 at vt.edu",
+          "password3",
+          "departmentNumber={1}",
+          "0823",
+          "givenName|sn",
+          "givenName=Joho|sn=Dof",
+        },
+        {
+          "jdoe4 at vt.edu",
+          "password4",
+          "departmentNumber={1}",
+          "0824",
+          "givenName|sn",
+          "givenName=Johp|sn=Dog",
+        },
+        {
+          "jdoe5 at vt.edu",
+          "password5",
+          "departmentNumber={1}",
+          "0825",
+          "givenName|sn",
+          "givenName=Johq|sn=Doh",
+        },
+        {
+          "jdoe6 at vt.edu",
+          "password6",
+          "departmentNumber={1}",
+          "0826",
+          "givenName|sn",
+          "givenName=Johr|sn=Doi",
+        },
+        {
+          "jdoe7 at vt.edu",
+          "password7",
+          "departmentNumber={1}",
+          "0827",
+          "givenName|sn",
+          "givenName=Johs|sn=Doj",
+        },
+        {
+          "jdoe8 at vt.edu",
+          "password8",
+          "departmentNumber={1}",
+          "0828",
+          "givenName|sn",
+          "givenName=Joht|sn=Dok",
+        },
+        {
+          "jdoe9 at vt.edu",
+          "password9",
+          "departmentNumber={1}",
+          "0829",
+          "givenName|sn",
+          "givenName=Johu|sn=Dol",
+        },
+        {
+          "jdoe10 at vt.edu",
+          "password10",
+          "departmentNumber={1}",
+          "0830",
+          "givenName|sn",
+          "givenName=Johv|sn=Dom",
+        },
+      };
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  filter  to authorize with.
+   * @param  filterArgs  to authorize with
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"authloadtest"},
+    dataProvider = "auth-data",
+    threadPoolSize = 50,
+    invocationCount = 1000,
+    timeOut = 60000
+  )
+  public void authenticateAndAuthorize(
+    final String user,
+    final String credential,
+    final String filter,
+    final String filterArgs,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    // test auth with return attributes
+    final Attributes attrs = this.singleTLSAuth.authenticate(
+      user,
+      credential,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorTest.java b/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorTest.java
new file mode 100644
index 0000000..c23681c
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/auth/AuthenticatorTest.java
@@ -0,0 +1,847 @@
+/*
+  $Id: AuthenticatorTest.java 2023 2011-07-11 14:50:38Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2023 $
+  Updated: $Date: 2011-07-11 15:50:38 +0100 (Mon, 11 Jul 2011) $
+*/
+package edu.vt.middleware.ldap.auth;
+
+import javax.naming.AuthenticationException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapConstants;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.auth.handler.AuthenticationResultHandler;
+import edu.vt.middleware.ldap.auth.handler.AuthorizationHandler;
+import edu.vt.middleware.ldap.auth.handler.CompareAuthenticationHandler;
+import edu.vt.middleware.ldap.auth.handler.TestAuthenticationResultHandler;
+import edu.vt.middleware.ldap.auth.handler.TestAuthorizationHandler;
+import edu.vt.middleware.ldap.bean.LdapAttributes;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link Authenticator}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2023 $
+ */
+public class AuthenticatorTest
+{
+
+  /** Invalid password test data. */
+  public static final String INVALID_PASSWD = "not-a-password";
+
+  /** Invalid filter test data. */
+  public static final String INVALID_FILTER = "departmentNumber=1111";
+
+  /** Entry created for auth tests. */
+  private static LdapEntry testLdapEntry;
+
+  /** Entry created for auth tests. */
+  private static LdapEntry specialCharsLdapEntry;
+
+  /** Ldap instance for concurrency testing. */
+  private Authenticator singleTLSAuth;
+
+  /** Ldap instance for concurrency testing. */
+  private Authenticator singleSSLAuth;
+
+  /** Ldap instance for concurrency testing. */
+  private Authenticator singleTLSDnAuth;
+
+  /** Ldap instance for concurrency testing. */
+  private Authenticator singleSSLDnAuth;
+
+
+  /**
+   * Default constructor.
+   *
+   * @throws  Exception  if ldap cannot be constructed
+   */
+  public AuthenticatorTest()
+    throws Exception
+  {
+    this.singleTLSAuth = TestUtil.createTLSAuthenticator();
+    this.singleSSLAuth = TestUtil.createSSLAuthenticator();
+    this.singleTLSDnAuth = TestUtil.createTLSDnAuthenticator();
+    this.singleSSLDnAuth = TestUtil.createSSLDnAuthenticator();
+  }
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry3" })
+  @BeforeClass(groups = {"authtest"})
+  public void createAuthEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createSpecialCharsEntry2" })
+  @BeforeClass(groups = {"authtest"})
+  public void createSpecialCharsEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    specialCharsLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      specialCharsLdapEntry.getDn(),
+      specialCharsLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+        specialCharsLdapEntry.getDn(),
+          new SearchFilter(specialCharsLdapEntry.getDn().split(",ou")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"authtest"})
+  public void deleteAuthEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.delete(specialCharsLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  createNew  whether to construct a new ldap instance.
+   *
+   * @return  <code>Authenticator</code>
+   *
+   * @throws  Exception  On ldap construction failure.
+   */
+  public Authenticator createTLSAuthenticator(final boolean createNew)
+    throws Exception
+  {
+    if (createNew) {
+      return TestUtil.createTLSAuthenticator();
+    }
+    return singleTLSAuth;
+  }
+
+
+  /**
+   * @param  createNew  whether to construct a new ldap instance.
+   *
+   * @return  <code>Authenticator</code>
+   *
+   * @throws  Exception  On ldap construction failure.
+   */
+  public Authenticator createTLSDnAuthenticator(final boolean createNew)
+    throws Exception
+  {
+    if (createNew) {
+      return TestUtil.createTLSDnAuthenticator();
+    }
+    return singleTLSDnAuth;
+  }
+
+
+  /**
+   * @param  createNew  whether to construct a new ldap instance.
+   *
+   * @return  <code>Authenticator</code>
+   *
+   * @throws  Exception  On ldap construction failure.
+   */
+  public Authenticator createSSLAuthenticator(final boolean createNew)
+    throws Exception
+  {
+    if (createNew) {
+      return TestUtil.createSSLAuthenticator();
+    }
+    return singleSSLAuth;
+  }
+
+
+  /**
+   * @param  createNew  whether to construct a new ldap instance.
+   *
+   * @return  <code>Authenticator</code>
+   *
+   * @throws  Exception  On ldap construction failure.
+   */
+  public Authenticator createSSLDnAuthenticator(final boolean createNew)
+    throws Exception
+  {
+    if (createNew) {
+      return TestUtil.createSSLDnAuthenticator();
+    }
+    return singleSSLDnAuth;
+  }
+
+
+  /**
+   * @param  ldapUrl  to check
+   * @param  baseDn  to check
+   */
+  @Parameters({ "loadPropertiesUrl", "loadPropertiesBaseDn" })
+  @Test(groups = {"authtest"})
+  public void loadProperties(final String ldapUrl, final String baseDn)
+  {
+    final Authenticator a = new Authenticator();
+    a.loadFromProperties(
+      TestUtil.class.getResourceAsStream("/ldap.tls.properties"));
+    AssertJUnit.assertEquals(ldapUrl, a.getAuthenticatorConfig().getLdapUrl());
+    AssertJUnit.assertEquals(baseDn, a.getAuthenticatorConfig().getBaseDn());
+  }
+
+
+  /**
+   * @param  uid  to get dn for.
+   * @param  user  to get dn for.
+   * @param  duplicateFilter  for user lookups
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "getDnUid", "getDnUser", "getDnDuplicateFilter" })
+  @Test(groups = {"authtest"})
+  public void getDn(
+    final String uid,
+    final String user,
+    final String duplicateFilter)
+    throws Exception
+  {
+    final Authenticator ldap = this.createTLSAuthenticator(true);
+
+    // test input
+    AssertJUnit.assertNull(ldap.getDn(null));
+    AssertJUnit.assertNull(ldap.getDn(""));
+
+    // test empty user field
+    final String[] userField = ldap.getAuthenticatorConfig().getUserField();
+    ldap.getAuthenticatorConfig().setUserField(new String[] {});
+    AssertJUnit.assertNull(ldap.getDn(user));
+    ldap.getAuthenticatorConfig().setUserField(userField);
+
+    // test construct dn
+    ldap.getAuthenticatorConfig().setConstructDn(true);
+    AssertJUnit.assertEquals(ldap.getDn(uid), testLdapEntry.getDn());
+    ldap.getAuthenticatorConfig().setConstructDn(false);
+
+    // test subtree searching
+    ldap.getAuthenticatorConfig().setSubtreeSearch(true);
+
+    final String baseDn = ldap.getAuthenticatorConfig().getBaseDn();
+    ldap.getAuthenticatorConfig().setBaseDn(
+      baseDn.substring(baseDn.indexOf(",") + 1));
+    AssertJUnit.assertEquals(ldap.getDn(user), testLdapEntry.getDn());
+    ldap.getAuthenticatorConfig().setBaseDn(baseDn);
+    ldap.getAuthenticatorConfig().setSubtreeSearch(false);
+
+    // test one level searching
+    AssertJUnit.assertEquals(ldap.getDn(user), testLdapEntry.getDn());
+
+    // test duplicate DNs
+    ldap.getAuthenticatorConfig().setUserFilter(duplicateFilter);
+    try {
+      ldap.getDn(user);
+      AssertJUnit.fail("Should have thrown NamingException");
+    } catch (Exception e) {
+      AssertJUnit.assertEquals(e.getClass(), NamingException.class);
+    }
+
+    ldap.getAuthenticatorConfig().setAllowMultipleDns(true);
+    ldap.getDn(user);
+
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateDn",
+      "authenticateDnCredential",
+      "authenticateDnReturnAttrs",
+      "authenticateDnResults"
+    }
+  )
+  @Test(
+    groups = {"authtest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void authenticateDn(
+    final String dn,
+    final String credential,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    // test plain auth
+    final Authenticator ldap = this.createTLSDnAuthenticator(false);
+    AssertJUnit.assertFalse(ldap.authenticate(dn, INVALID_PASSWD));
+    AssertJUnit.assertTrue(ldap.authenticate(dn, credential));
+
+    // test auth with return attributes
+    final Attributes attrs = ldap.authenticate(
+      dn,
+      credential,
+      returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+  }
+
+
+  /**
+   * @param  dn  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateDn",
+      "authenticateDnCredential",
+      "authenticateDnReturnAttrs",
+      "authenticateDnResults"
+    }
+  )
+  @Test(
+    groups = {"authtest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void authenticateDnSsl(
+    final String dn,
+    final String credential,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    // test plain auth
+    final Authenticator ldap = this.createSSLDnAuthenticator(false);
+    AssertJUnit.assertFalse(ldap.authenticate(dn, INVALID_PASSWD));
+    AssertJUnit.assertTrue(ldap.authenticate(dn, credential));
+
+    // test auth with return attributes
+    final Attributes attrs = ldap.authenticate(
+      dn,
+      credential,
+      returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+  }
+
+
+  /**
+   * @param  dn  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  filter  to authorize with.
+   * @param  filterArgs  to authorize with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateDn",
+      "authenticateDnCredential",
+      "authenticateDnFilter",
+      "authenticateDnFilterArgs",
+      "authenticateDnReturnAttrs",
+      "authenticateDnResults"
+    }
+  )
+  @Test(
+    groups = {"authtest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void authenticateDnAndAuthorize(
+    final String dn,
+    final String credential,
+    final String filter,
+    final String filterArgs,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    final Authenticator ldap = this.createTLSDnAuthenticator(false);
+
+    // test plain auth
+    AssertJUnit.assertFalse(
+      ldap.authenticate(dn, INVALID_PASSWD, new SearchFilter(filter)));
+    AssertJUnit.assertFalse(
+      ldap.authenticate(dn, credential, new SearchFilter(INVALID_FILTER)));
+    AssertJUnit.assertTrue(
+      ldap.authenticate(
+        dn,
+        credential,
+        new SearchFilter(filter, filterArgs.split("\\|"))));
+
+    // test auth with return attributes
+    final Attributes attrs = ldap.authenticate(
+      dn,
+      credential,
+      new SearchFilter(filter, filterArgs.split("\\|")),
+      returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+  }
+
+
+  /**
+   * @param  dn  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  filter  to authorize with.
+   * @param  filterArgs  to authorize with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateDn",
+      "authenticateDnCredential",
+      "authenticateDnFilter",
+      "authenticateDnFilterArgs"
+    }
+  )
+  @Test(groups = {"authtest"})
+  public void authenticateDnHandler(
+    final String dn,
+    final String credential,
+    final String filter,
+    final String filterArgs)
+    throws Exception
+  {
+    // test authenticator handler
+    final Authenticator ldap = this.createTLSDnAuthenticator(true);
+    final TestAuthenticationResultHandler authHandler =
+      new TestAuthenticationResultHandler();
+    ldap.getAuthenticatorConfig().setAuthenticationResultHandlers(
+      new AuthenticationResultHandler[] {authHandler});
+
+    final TestAuthorizationHandler authzHandler =
+      new TestAuthorizationHandler();
+    ldap.getAuthenticatorConfig().setAuthorizationHandlers(
+      new AuthorizationHandler[] {authzHandler});
+
+    AssertJUnit.assertFalse(ldap.authenticate(dn, INVALID_PASSWD));
+    AssertJUnit.assertFalse(authHandler.getResults().get(dn).booleanValue());
+    AssertJUnit.assertFalse(!authzHandler.getResults().isEmpty());
+
+    AssertJUnit.assertFalse(ldap.authenticate(dn, credential));
+    AssertJUnit.assertFalse(authHandler.getResults().get(dn).booleanValue());
+    AssertJUnit.assertFalse(!authzHandler.getResults().isEmpty());
+
+    authzHandler.setSucceed(true);
+
+    AssertJUnit.assertTrue(ldap.authenticate(dn, credential));
+    AssertJUnit.assertTrue(authHandler.getResults().get(dn).booleanValue());
+    AssertJUnit.assertTrue(authzHandler.getResults().get(0).equals(dn));
+
+    authHandler.getResults().clear();
+    authzHandler.getResults().clear();
+
+    AssertJUnit.assertTrue(
+      ldap.authenticate(
+        dn,
+        credential,
+        new SearchFilter(filter, filterArgs.split("\\|"))));
+    AssertJUnit.assertTrue(authHandler.getResults().get(dn).booleanValue());
+    AssertJUnit.assertTrue(authzHandler.getResults().get(0).equals(dn));
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "digestMd5User", "digestMd5Credential" })
+  @Test(groups = {"authtest"})
+  public void authenticateDigestMd5(final String user, final String credential)
+    throws Exception
+  {
+    final Authenticator ldap = TestUtil.createDigestMD5Authenticator();
+    AssertJUnit.assertFalse(ldap.authenticate(user, INVALID_PASSWD));
+    AssertJUnit.assertTrue(ldap.authenticate(user, credential));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "cramMd5User", "cramMd5Credential" })
+  @Test(groups = {"authtest"})
+  public void authenticateCramMd5(final String user, final String credential)
+    throws Exception
+  {
+    final Authenticator ldap = TestUtil.createCramMD5Authenticator();
+    AssertJUnit.assertFalse(ldap.authenticate(user, INVALID_PASSWD));
+    AssertJUnit.assertTrue(ldap.authenticate(user, credential));
+    ldap.close();
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateUser",
+      "authenticateCredential",
+      "authenticateReturnAttrs",
+      "authenticateResults"
+    }
+  )
+  @Test(
+    groups = {"authtest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void authenticate(
+    final String user,
+    final String credential,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    final Authenticator ldap = this.createTLSAuthenticator(false);
+
+    // test plain auth
+    AssertJUnit.assertFalse(ldap.authenticate(user, INVALID_PASSWD));
+    AssertJUnit.assertTrue(ldap.authenticate(user, credential));
+
+    // test auth with return attributes
+    final Attributes attrs = ldap.authenticate(
+      user,
+      credential,
+      returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateUser",
+      "authenticateCredential",
+      "authenticateReturnAttrs",
+      "authenticateResults"
+    }
+  )
+  @Test(
+    groups = {"authtest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void authenticateSsl(
+    final String user,
+    final String credential,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    final Authenticator ldap = this.createSSLAuthenticator(false);
+
+    // test plain auth
+    AssertJUnit.assertFalse(ldap.authenticate(user, INVALID_PASSWD));
+    AssertJUnit.assertTrue(ldap.authenticate(user, credential));
+
+    // test auth with return attributes
+    final Attributes attrs = ldap.authenticate(
+      user,
+      credential,
+      returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  filter  to authorize with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateUser",
+      "authenticateCredential",
+      "authenticateFilter",
+      "authenticateReturnAttrs",
+      "authenticateResults"
+    }
+  )
+  @Test(
+    groups = {"authtest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void authenticateAndAuthorize(
+    final String user,
+    final String credential,
+    final String filter,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    final Authenticator ldap = this.createTLSAuthenticator(false);
+
+    // test plain auth
+    AssertJUnit.assertFalse(
+      ldap.authenticate(user, INVALID_PASSWD, new SearchFilter(filter)));
+    AssertJUnit.assertFalse(
+      ldap.authenticate(user, credential, new SearchFilter(INVALID_FILTER)));
+    AssertJUnit.assertTrue(
+      ldap.authenticate(user, credential, new SearchFilter(filter)));
+
+    // test auth with return attributes
+    final Attributes attrs = ldap.authenticate(
+      user,
+      credential,
+      new SearchFilter(filter),
+      returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  filter  to authorize with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateUser",
+      "authenticateCredential",
+      "authenticateFilter",
+      "authenticateReturnAttrs",
+      "authenticateResults"
+    }
+  )
+  @Test(
+    groups = {"authtest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void authenticateAndAuthorizeCompare(
+    final String user,
+    final String credential,
+    final String filter,
+    final String returnAttrs,
+    final String results)
+    throws Exception
+  {
+    final Authenticator ldap = this.createTLSAuthenticator(true);
+    ldap.getAuthenticatorConfig().setAuthenticationHandler(
+      new CompareAuthenticationHandler());
+
+    // test plain auth
+    AssertJUnit.assertFalse(
+      ldap.authenticate(user, INVALID_PASSWD, new SearchFilter(filter)));
+    AssertJUnit.assertFalse(
+      ldap.authenticate(user, credential, new SearchFilter(INVALID_FILTER)));
+    AssertJUnit.assertTrue(
+      ldap.authenticate(user, credential, new SearchFilter(filter)));
+
+    // test auth with return attributes
+    final Attributes attrs = ldap.authenticate(
+      user,
+      credential,
+      new SearchFilter(filter),
+      returnAttrs.split("\\|"));
+    final LdapAttributes expected = TestUtil.convertStringToAttributes(results);
+    AssertJUnit.assertEquals(expected, TestUtil.newLdapAttributes(attrs));
+
+    ldap.close();
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   * @param  returnAttrs  to search for.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateUser",
+      "authenticateCredential",
+      "authenticateReturnAttrs"
+    }
+  )
+  @Test(groups = {"authtest"})
+  public void authenticateExceptions(
+    final String user,
+    final String credential,
+    final String returnAttrs)
+    throws Exception
+  {
+    final Authenticator ldap = this.createTLSAuthenticator(true);
+
+    try {
+      ldap.authenticate(user, new Object(), returnAttrs.split("\\|"));
+      AssertJUnit.fail("Should have thrown AuthenticationException");
+    } catch (Exception e) {
+      AssertJUnit.assertEquals(e.getClass(), AuthenticationException.class);
+    }
+
+    try {
+      ldap.authenticate(null, credential, returnAttrs.split("\\|"));
+      AssertJUnit.fail("Should have thrown AuthenticationException");
+    } catch (Exception e) {
+      AssertJUnit.assertEquals(e.getClass(), AuthenticationException.class);
+    }
+
+    try {
+      ldap.authenticate("", credential, returnAttrs.split("\\|"));
+      AssertJUnit.fail("Should have thrown AuthenticationException");
+    } catch (Exception e) {
+      AssertJUnit.assertEquals(e.getClass(), AuthenticationException.class);
+    }
+
+    // must do this test after the search connection has been setup or
+    // the anon auth will block the search
+    ldap.getAuthenticatorConfig().setAuthtype(LdapConstants.NONE_AUTHTYPE);
+    try {
+      ldap.authenticate(user, credential, returnAttrs.split("\\|"));
+      AssertJUnit.fail("Should have thrown AuthenticationException");
+    } catch (Exception e) {
+      AssertJUnit.assertEquals(e.getClass(), AuthenticationException.class);
+    }
+
+    ldap.getAuthenticatorConfig().setAuthtype(LdapConstants.SIMPLE_AUTHTYPE);
+    try {
+      ldap.authenticate(
+        user,
+        credential,
+        new SearchFilter(INVALID_FILTER),
+        returnAttrs.split("\\|"));
+      AssertJUnit.fail("Should have thrown AuthorizationException");
+    } catch (Exception e) {
+      AssertJUnit.assertEquals(e.getClass(), AuthorizationException.class);
+    }
+
+    ldap.close();
+  }
+
+
+  /**
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "authenticateSpecialCharsUser",
+      "authenticateSpecialCharsCredential"
+    }
+  )
+  @Test(groups = {"authtest"})
+  public void authenticateSpecialChars(
+    final String user, final String credential)
+    throws Exception
+  {
+    final Authenticator ldap = this.createTLSAuthenticator(true);
+
+    // test without rewrite
+    AssertJUnit.assertFalse(ldap.authenticate(user, INVALID_PASSWD));
+    AssertJUnit.assertTrue(ldap.authenticate(user, credential));
+
+    // test with rewrite
+    ldap.getAuthenticatorConfig().setBaseDn("dc=blah");
+    ldap.getAuthenticatorConfig().setSubtreeSearch(true);
+    AssertJUnit.assertFalse(ldap.authenticate(user, INVALID_PASSWD));
+    AssertJUnit.assertTrue(ldap.authenticate(user, credential));
+
+    ldap.close();
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/auth/handler/TestAuthenticationResultHandler.java b/src/test/java/edu/vt/middleware/ldap/auth/handler/TestAuthenticationResultHandler.java
new file mode 100644
index 0000000..62dd99d
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/auth/handler/TestAuthenticationResultHandler.java
@@ -0,0 +1,49 @@
+/*
+  $Id: TestAuthenticationResultHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <code>TestAuthenticationResultHandler</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class TestAuthenticationResultHandler
+  implements AuthenticationResultHandler
+{
+
+  /** results. */
+  private Map<String, Boolean> results = new HashMap<String, Boolean>();
+
+
+  /** {@inheritDoc} */
+  public void process(final AuthenticationCriteria ac, final boolean success)
+  {
+    this.results.put(ac.getDn(), Boolean.valueOf(success));
+  }
+
+
+  /**
+   * Returns the authentication results.
+   *
+   * @return  authentication results
+   */
+  public Map<String, Boolean> getResults()
+  {
+    return this.results;
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/auth/handler/TestAuthorizationHandler.java b/src/test/java/edu/vt/middleware/ldap/auth/handler/TestAuthorizationHandler.java
new file mode 100644
index 0000000..abed850
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/auth/handler/TestAuthorizationHandler.java
@@ -0,0 +1,69 @@
+/*
+  $Id: TestAuthorizationHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.auth.handler;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+import edu.vt.middleware.ldap.auth.AuthorizationException;
+
+/**
+ * <code>TestAuthenticationResultHandler</code>.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class TestAuthorizationHandler implements AuthorizationHandler
+{
+
+  /** results. */
+  private List<String> results = new ArrayList<String>();
+
+  /** whether process should succeed. */
+  private boolean succeed;
+
+
+  /** {@inheritDoc} */
+  public void process(final AuthenticationCriteria ac, final LdapContext ctx)
+    throws NamingException
+  {
+    if (!succeed) {
+      throw new AuthorizationException("Succeed is false");
+    }
+    this.results.add(ac.getDn());
+  }
+
+
+  /**
+   * Returns the authentication results.
+   *
+   * @return  authentication results
+   */
+  public List<String> getResults()
+  {
+    return this.results;
+  }
+
+
+  /**
+   * Sets whether process will succeed.
+   *
+   * @param  b  <code>boolean</code>
+   */
+  public void setSucceed(final boolean b)
+  {
+    succeed = b;
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/bean/LdapResultTest.java b/src/test/java/edu/vt/middleware/ldap/bean/LdapResultTest.java
new file mode 100644
index 0000000..e823b38
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/bean/LdapResultTest.java
@@ -0,0 +1,111 @@
+/*
+  $Id: LdapResultTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.bean;
+
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link LdapResult}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdapResultTest
+{
+
+  /** Entry created for tests. */
+  private static LdapEntry testLdapEntry;
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry4" })
+  @BeforeClass(groups = {"beantest"})
+  public void createLdapEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"beantest"})
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search for.
+   * @param  filter  to search with.
+   * @param  returnAttrs  attributes to return from search
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "toSearchResultsDn",
+      "toSearchResultsFilter",
+      "toSearchResultsAttrs",
+      "toSearchResultsResults"
+    }
+  )
+  @Test(groups = {"beantest"})
+  public void toSearchResults(
+    final String dn,
+    final String filter,
+    final String returnAttrs,
+    final String ldifFile)
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createLdap();
+
+    final LdapResult found = TestUtil.newLdapResult(
+      ldap.search(dn, new SearchFilter(filter), returnAttrs.split("\\|")));
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    final LdapResult expected = TestUtil.convertLdifToResult(ldif);
+    AssertJUnit.assertEquals(expected, found);
+    ldap.close();
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/dsml/DsmlTest.java b/src/test/java/edu/vt/middleware/ldap/dsml/DsmlTest.java
new file mode 100644
index 0000000..3598679
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/dsml/DsmlTest.java
@@ -0,0 +1,207 @@
+/*
+  $Id: DsmlTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.dsml;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Iterator;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import edu.vt.middleware.ldap.bean.SortedLdapBeanFactory;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link Dsmlv1} and {@link Dsmlv2}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class DsmlTest
+{
+
+  /** Entry created for ldap tests. */
+  private static LdapEntry testLdapEntry;
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry11" })
+  @BeforeClass(groups = {"dsmltest"})
+  public void createLdapEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"dsmltest"})
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "dsmlSearchDn",
+      "dsmlSearchFilter"
+    })
+  @Test(groups = {"dsmltest"})
+  public void searchAndCompareDsmlv1(final String dn, final String filter)
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createLdap();
+    final Dsmlv1 dsml = new Dsmlv1();
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter));
+
+    final LdapResult result1 = TestUtil.newLdapResult(iter);
+    final StringWriter writer = new StringWriter();
+    dsml.outputDsml(result1.toSearchResults().iterator(), writer);
+
+    final StringReader reader = new StringReader(writer.toString());
+    final LdapResult result2 = dsml.importDsmlToLdapResult(reader);
+
+    AssertJUnit.assertEquals(result1, result2);
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dsmlFile  to test with.
+   * @param  dsmlSortedFile  to test with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "dsmlv1Entry",
+      "dsmlv1SortedEntry"
+    })
+  @Test(groups = {"dsmltest"})
+  public void readAndCompareDsmlv1(
+    final String dsmlFile,
+    final String dsmlSortedFile)
+    throws Exception
+  {
+    final Dsmlv1 dsml = new Dsmlv1();
+    dsml.setLdapBeanFactory(new SortedLdapBeanFactory());
+
+    final String dsmlStringSorted = TestUtil.readFileIntoString(dsmlSortedFile);
+    final Iterator<SearchResult> iter = dsml.importDsml(
+      new StringReader(TestUtil.readFileIntoString(dsmlFile)));
+    final StringWriter writer = new StringWriter();
+    dsml.outputDsml(iter, writer);
+
+    AssertJUnit.assertEquals(dsmlStringSorted, writer.toString());
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "dsmlSearchDn",
+      "dsmlSearchFilter"
+    })
+  @Test(groups = {"dsmltest"})
+  public void searchAndCompareDsmlv2(final String dn, final String filter)
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createLdap();
+    final Dsmlv2 dsml = new Dsmlv2();
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter));
+
+    final LdapResult result1 = TestUtil.newLdapResult(iter);
+    final StringWriter writer = new StringWriter();
+    dsml.outputDsml(result1.toSearchResults().iterator(), writer);
+
+    final StringReader reader = new StringReader(writer.toString());
+    final LdapResult result2 = dsml.importDsmlToLdapResult(reader);
+
+    AssertJUnit.assertEquals(result1, result2);
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dsmlFile  to test with.
+   * @param  dsmlSortedFile  to test with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "dsmlv2Entry",
+      "dsmlv2SortedEntry"
+    })
+  @Test(groups = {"dsmltest"})
+  public void readAndCompareDsmlv2(
+    final String dsmlFile,
+    final String dsmlSortedFile)
+    throws Exception
+  {
+    final Dsmlv2 dsml = new Dsmlv2();
+    dsml.setLdapBeanFactory(new SortedLdapBeanFactory());
+
+    final String dsmlStringSorted = TestUtil.readFileIntoString(dsmlSortedFile);
+    final Iterator<SearchResult> iter = dsml.importDsml(
+      new StringReader(TestUtil.readFileIntoString(dsmlFile)));
+    final StringWriter writer = new StringWriter();
+    dsml.outputDsml(iter, writer);
+
+    AssertJUnit.assertEquals(dsmlStringSorted, writer.toString());
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/jaas/LdapLoginModuleTest.java b/src/test/java/edu/vt/middleware/ldap/jaas/LdapLoginModuleTest.java
new file mode 100644
index 0000000..d208d3f
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/jaas/LdapLoginModuleTest.java
@@ -0,0 +1,771 @@
+/*
+  $Id: LdapLoginModuleTest.java 2218 2012-01-23 19:58:08Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2218 $
+  Updated: $Date: 2012-01-23 19:58:08 +0000 (Mon, 23 Jan 2012) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.security.Principal;
+import java.security.acl.Group;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import edu.vt.middleware.ldap.AttributesFactory;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.Ldap.AttributeModification;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link LdapLoginModule}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2218 $
+ */
+public class LdapLoginModuleTest
+{
+
+  /** Invalid password test data. */
+  public static final String INVALID_PASSWD = "not-a-password";
+
+  /** Entry created for auth tests. */
+  private static LdapEntry testLdapEntry;
+
+  /** Entries for group tests. */
+  private static Map<String, LdapEntry[]> groupEntries =
+    new HashMap<String, LdapEntry[]>();
+
+  /**
+   * Initialize the map of group entries.
+   */
+  static {
+    for (int i = 6; i <= 9; i++) {
+      groupEntries.put(String.valueOf(i), new LdapEntry[2]);
+    }
+  }
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry7" })
+  @BeforeClass(groups = {"jaastest"})
+  public void createAuthEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+
+    System.setProperty(
+      "java.security.auth.login.config",
+      "src/test/resources/ldap_jaas.config");
+  }
+
+
+  /**
+   * @param  ldifFile6  to create.
+   * @param  ldifFile7  to create.
+   * @param  ldifFile8  to create.
+   * @param  ldifFile9  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "createGroup6",
+      "createGroup7",
+      "createGroup8",
+      "createGroup9"
+    }
+  )
+  @BeforeClass(groups = {"jaastest"})
+  public void createGroupEntry(
+    final String ldifFile6,
+    final String ldifFile7,
+    final String ldifFile8,
+    final String ldifFile9)
+    throws Exception
+  {
+    groupEntries.get("6")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile6));
+    groupEntries.get("7")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile7));
+    groupEntries.get("8")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile8));
+    groupEntries.get("9")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile9));
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    for (Map.Entry<String, LdapEntry[]> e : groupEntries.entrySet()) {
+      ldap.create(
+        e.getValue()[0].getDn(),
+        e.getValue()[0].getLdapAttributes().toAttributes());
+    }
+    ldap.close();
+
+    ldap = TestUtil.createLdap();
+    for (Map.Entry<String, LdapEntry[]> e : groupEntries.entrySet()) {
+      while (
+        !ldap.compare(
+            e.getValue()[0].getDn(),
+            new SearchFilter(e.getValue()[0].getDn().split(",")[0]))) {
+        Thread.sleep(100);
+      }
+    }
+
+    // setup group relationships
+    ldap.modifyAttributes(
+      groupEntries.get("6")[0].getDn(),
+      AttributeModification.ADD,
+      AttributesFactory.createAttributes(
+        "member",
+        new String[] {
+          "uid=7,ou=test,dc=vt,dc=edu",
+          "uugid=group7,ou=test,dc=vt,dc=edu",
+        }));
+    ldap.modifyAttributes(
+      groupEntries.get("7")[0].getDn(),
+      AttributeModification.ADD,
+      AttributesFactory.createAttributes(
+        "member",
+        new String[] {
+          "uugid=group8,ou=test,dc=vt,dc=edu",
+          "uugid=group9,ou=test,dc=vt,dc=edu",
+        }));
+    ldap.modifyAttributes(
+      groupEntries.get("8")[0].getDn(),
+      AttributeModification.ADD,
+      AttributesFactory.createAttributes(
+        "member",
+        "uugid=group7,ou=test,dc=vt,dc=edu"));
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"jaastest"})
+  public void deleteAuthEntry()
+    throws Exception
+  {
+    System.clearProperty("java.security.auth.login.config");
+
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.delete(groupEntries.get("6")[0].getDn());
+    ldap.delete(groupEntries.get("7")[0].getDn());
+    ldap.delete(groupEntries.get("8")[0].getDn());
+    ldap.delete(groupEntries.get("9")[0].getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRole", "jaasCredential" })
+  @Test(
+    groups = {"jaastest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void contextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap", dn, user, role, credential, false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRole", "jaasCredential" })
+  @Test(
+    groups = {"jaastest"},
+    enabled = false, // requires JVM wide truststore to pass
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void contextSslTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-ssl", dn, user, role, credential, false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRole", "jaasCredential" })
+  @Test(
+    groups = {"jaastest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void contextSsl2Test(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-ssl-2", dn, user, role, credential, false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRole", "jaasCredential" })
+  @Test(
+    groups = {"jaastest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void authzContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-authz", dn, user, role, credential, true);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRole", "jaasCredential" })
+  @Test(
+    groups = {"jaastest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void randomContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-random", dn, user, role, credential, true);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasCredential" })
+  @Test(
+    groups = {"jaastest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void filterContextTest(
+    final String dn,
+    final String user,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-filter", dn, user, "", credential, false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRole", "jaasCredential" })
+  @Test(groups = {"jaastest"})
+  public void handlerContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    final TestCallbackHandler callback = new TestCallbackHandler();
+    callback.setName(user);
+    callback.setPassword(credential);
+
+    final LoginContext lc = new LoginContext("vt-ldap-handler", callback);
+    try {
+      lc.login();
+      AssertJUnit.fail(
+        "Handler succeed set to false, login should have failed");
+    } catch (Exception e) {
+      AssertJUnit.assertEquals(e.getClass(), LoginException.class);
+    }
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasRoleCombined", "jaasCredential" })
+  @Test(
+    groups = {"jaastest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void rolesContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-roles", dn, user, role, credential, false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "jaasDn", "jaasUser", "jaasRoleCombinedRecursive", "jaasCredential"
+    }
+  )
+  @Test(groups = {"jaastest"})
+  public void rolesRecursiveContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest(
+      "vt-ldap-roles-recursive",
+      dn,
+      user,
+      role,
+      credential,
+      false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRoleDefault", "jaasCredential" })
+  @Test(groups = {"jaastest"})
+  public void useFirstContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-use-first", dn, user, role, credential, false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasRoleCombined", "jaasCredential" })
+  @Test(groups = {"jaastest"})
+  public void tryFirstContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-try-first", dn, user, role, credential, false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRole", "jaasCredential" })
+  @Test(groups = {"jaastest"})
+  public void sufficientContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-sufficient", dn, user, role, credential, false);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasUserRole", "jaasCredential" })
+  @Test(groups = {"jaastest"})
+  public void oldContextTest(
+    final String dn,
+    final String user,
+    final String role,
+    final String credential)
+    throws Exception
+  {
+    this.doContextTest("vt-ldap-deprecated", dn, user, role, credential, false);
+  }
+
+
+  /**
+   * @param  name  of the jaas configuration
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   * @param  credential  to authenticate with.
+   * @param  checkLdapDn  whether to check the LdapDnPrincipal
+   *
+   * @throws  Exception  On test failure.
+   */
+  private void doContextTest(
+    final String name,
+    final String dn,
+    final String user,
+    final String role,
+    final String credential,
+    final boolean checkLdapDn)
+    throws Exception
+  {
+    final TestCallbackHandler callback = new TestCallbackHandler();
+    callback.setName(user);
+    callback.setPassword(INVALID_PASSWD);
+
+    LoginContext lc = new LoginContext(name, callback);
+    try {
+      lc.login();
+      AssertJUnit.fail("Invalid password, login should have failed");
+    } catch (Exception e) {
+      AssertJUnit.assertEquals(e.getClass(), LoginException.class);
+    }
+
+    callback.setPassword(credential);
+    lc = new LoginContext(name, callback);
+    try {
+      lc.login();
+    } catch (Exception e) {
+      AssertJUnit.fail(e.getMessage());
+    }
+
+    final Set<LdapPrincipal> principals = lc.getSubject().getPrincipals(
+      LdapPrincipal.class);
+    AssertJUnit.assertEquals(1, principals.size());
+
+    final LdapPrincipal p = principals.iterator().next();
+    AssertJUnit.assertEquals(p.getName(), user);
+    if (!"".equals(role)) {
+      AssertJUnit.assertTrue(p.getLdapAttributes().size() > 0);
+    }
+
+    final Set<LdapDnPrincipal> dnPrincipals = lc.getSubject().getPrincipals(
+      LdapDnPrincipal.class);
+    if (checkLdapDn) {
+      AssertJUnit.assertEquals(1, dnPrincipals.size());
+
+      final LdapDnPrincipal dnP = dnPrincipals.iterator().next();
+      AssertJUnit.assertEquals(dnP.getName(), dn);
+      if (!"".equals(role)) {
+        AssertJUnit.assertTrue(dnP.getLdapAttributes().size() > 0);
+      }
+    } else {
+      AssertJUnit.assertEquals(0, dnPrincipals.size());
+    }
+
+    final Set<LdapRole> roles = lc.getSubject().getPrincipals(LdapRole.class);
+
+    final Iterator<LdapRole> roleIter = roles.iterator();
+    String[] checkRoles = role.split("\\|");
+    if (checkRoles.length == 1 && "".equals(checkRoles[0])) {
+      checkRoles = new String[0];
+    }
+    AssertJUnit.assertEquals(checkRoles.length, roles.size());
+    while (roleIter.hasNext()) {
+      final LdapRole r = roleIter.next();
+      boolean match = false;
+      for (String s : checkRoles) {
+        if (s.equals(r.getName())) {
+          match = true;
+        }
+      }
+      AssertJUnit.assertTrue(match);
+    }
+
+    final Set<LdapCredential> credentials = lc.getSubject()
+        .getPrivateCredentials(LdapCredential.class);
+    AssertJUnit.assertEquals(1, credentials.size());
+
+    final LdapCredential c = credentials.iterator().next();
+    AssertJUnit.assertEquals(
+      new String((char[]) c.getCredential()),
+      credential);
+
+    try {
+      lc.logout();
+    } catch (Exception e) {
+      AssertJUnit.fail(e.getMessage());
+    }
+
+    AssertJUnit.assertEquals(0, lc.getSubject().getPrincipals().size());
+    AssertJUnit.assertEquals(0, lc.getSubject().getPrivateCredentials().size());
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasRoleCombined" })
+  @Test(
+    groups = {"jaastest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void rolesOnlyContextTest(
+    final String dn,
+    final String user,
+    final String role)
+    throws Exception
+  {
+    this.doRolesContextTest("vt-ldap-roles-only", dn, user, role);
+  }
+
+
+  /**
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "jaasDn", "jaasUser", "jaasRoleCombined" })
+  @Test(
+    groups = {"jaastest"},
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void dnRolesOnlyContextTest(
+    final String dn,
+    final String user,
+    final String role)
+    throws Exception
+  {
+    this.doRolesContextTest("vt-ldap-dn-roles-only", dn, user, role);
+  }
+
+
+  /**
+   * @param  name  of the jaas configuration
+   * @param  dn  of this user
+   * @param  user  to authenticate.
+   * @param  role  to set for this user
+   *
+   * @throws  Exception  On test failure.
+   */
+  private void doRolesContextTest(
+    final String name,
+    final String dn,
+    final String user,
+    final String role)
+    throws Exception
+  {
+    final TestCallbackHandler callback = new TestCallbackHandler();
+    callback.setName(user);
+
+    final LoginContext lc = new LoginContext(name, callback);
+    try {
+      lc.login();
+    } catch (Exception e) {
+      AssertJUnit.fail(e.getMessage());
+    }
+
+    final Set<LdapRole> roles = lc.getSubject().getPrincipals(LdapRole.class);
+
+    final Iterator<LdapRole> roleIter = roles.iterator();
+    final String[] checkRoles = role.split("\\|");
+    AssertJUnit.assertEquals(checkRoles.length, roles.size());
+    while (roleIter.hasNext()) {
+      final LdapRole r = roleIter.next();
+      boolean match = false;
+      for (String s : checkRoles) {
+        if (s.equals(r.getName())) {
+          match = true;
+        }
+      }
+      AssertJUnit.assertTrue(match);
+    }
+
+    final Set<Group> roleGroups = lc.getSubject().getPrincipals(Group.class);
+    AssertJUnit.assertTrue(roleGroups.size() == 2);
+    for (Group g : roleGroups) {
+      if ("Roles".equals(g.getName())) {
+        final Enumeration<? extends Principal> members = g.members();
+        int count = 0;
+        while (members.hasMoreElements()) {
+          final Principal p = members.nextElement();
+          boolean match = false;
+          for (LdapRole lr : lc.getSubject().getPrincipals(LdapRole.class)) {
+            if (lr.getName().equals(p.getName())) {
+              match = true;
+            }
+          }
+          AssertJUnit.assertTrue(match);
+          count++;
+        }
+        AssertJUnit.assertEquals(
+          count,
+          lc.getSubject().getPrincipals(LdapRole.class).size());
+      } else if ("Principals".equals(g.getName())) {
+        final Enumeration<? extends Principal> members = g.members();
+        int count = 0;
+        while (members.hasMoreElements()) {
+          final Principal p = members.nextElement();
+          boolean match = false;
+          for (
+            LdapPrincipal lp :
+              lc.getSubject().getPrincipals(LdapPrincipal.class)) {
+            if (lp.getName().equals(p.getName())) {
+              match = true;
+            }
+          }
+          AssertJUnit.assertTrue(match);
+          count++;
+        }
+        AssertJUnit.assertEquals(
+          count,
+          lc.getSubject().getPrincipals(LdapPrincipal.class).size());
+      } else {
+        AssertJUnit.fail("Found invalid group");
+      }
+    }
+
+    final Set<?> credentials = lc.getSubject().getPrivateCredentials();
+    AssertJUnit.assertEquals(0, credentials.size());
+
+    try {
+      lc.logout();
+    } catch (Exception e) {
+      AssertJUnit.fail(e.getMessage());
+    }
+
+    AssertJUnit.assertEquals(0, lc.getSubject().getPrincipals().size());
+    AssertJUnit.assertEquals(0, lc.getSubject().getPrivateCredentials().size());
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/jaas/TestCallbackHandler.java b/src/test/java/edu/vt/middleware/ldap/jaas/TestCallbackHandler.java
new file mode 100644
index 0000000..5661e17
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/jaas/TestCallbackHandler.java
@@ -0,0 +1,77 @@
+/*
+  $Id: TestCallbackHandler.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.io.IOException;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+/**
+ * Class that implements a callback handler to help with jaas testing.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class TestCallbackHandler implements CallbackHandler
+{
+
+  /** test name. */
+  private String name;
+
+  /** test password. */
+  private String password;
+
+
+  /** @param  s  to set name with */
+  public void setName(final String s)
+  {
+    this.name = s;
+  }
+
+
+  /** @param  s  to set password with */
+  public void setPassword(final String s)
+  {
+    this.password = s;
+  }
+
+
+  /**
+   * @param  callbacks  to handle
+   *
+   * @throws  IOException  if an input or output error occurs
+   * @throws  UnsupportedCallbackException  if a supplied callback cannot be
+   * handled
+   */
+  public void handle(final Callback[] callbacks)
+    throws IOException, UnsupportedCallbackException
+  {
+    for (int i = 0; i < callbacks.length; i++) {
+      if (callbacks[i] instanceof NameCallback) {
+        final NameCallback nc = (NameCallback) callbacks[i];
+        nc.setName(name);
+      } else if (callbacks[i] instanceof PasswordCallback) {
+        final PasswordCallback pc = (PasswordCallback) callbacks[i];
+        if (password != null) {
+          pc.setPassword(password.toCharArray());
+        }
+      } else {
+        throw new UnsupportedCallbackException(callbacks[i], "Unsupported");
+      }
+    }
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/jaas/TestLoginModule.java b/src/test/java/edu/vt/middleware/ldap/jaas/TestLoginModule.java
new file mode 100644
index 0000000..34173d8
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/jaas/TestLoginModule.java
@@ -0,0 +1,112 @@
+/*
+  $Id: TestLoginModule.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.jaas;
+
+import java.io.IOException;
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+/**
+ * <code>TestLoginModule</code> is a test login module.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class TestLoginModule implements LoginModule
+{
+
+  /** Initialized subject. */
+  protected Subject subject;
+
+  /** Initialized callback handler. */
+  protected CallbackHandler callbackHandler;
+
+  /** Shared state from other login module. */
+  @SuppressWarnings("unchecked")
+  protected Map sharedState;
+
+  /** Whether authentication was successful. */
+  protected boolean success;
+
+
+  /** {@inheritDoc} */
+  public void initialize(
+    final Subject subject,
+    final CallbackHandler callbackHandler,
+    final Map<String, ?> sharedState,
+    final Map<String, ?> options)
+  {
+    this.subject = subject;
+    this.callbackHandler = callbackHandler;
+    this.sharedState = sharedState;
+  }
+
+
+  /** {@inheritDoc} */
+  @SuppressWarnings("unchecked")
+  public boolean login()
+    throws LoginException
+  {
+    try {
+      final NameCallback nameCb = new NameCallback("Enter user: ");
+      final PasswordCallback passCb = new PasswordCallback(
+        "Enter user password: ",
+        false);
+      this.callbackHandler.handle(new Callback[] {nameCb, passCb});
+
+      this.sharedState.put(LdapLoginModule.LOGIN_NAME, nameCb.getName());
+      this.sharedState.put(
+        LdapLoginModule.LOGIN_PASSWORD,
+        passCb.getPassword());
+      this.success = true;
+    } catch (IOException e) {
+      this.success = false;
+      throw new LoginException(e.toString());
+    } catch (UnsupportedCallbackException e) {
+      this.success = false;
+      throw new LoginException(e.toString());
+    }
+    return true;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean commit()
+    throws LoginException
+  {
+    return true;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean abort()
+  {
+    this.success = false;
+    return true;
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean logout()
+  {
+    return true;
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/ldif/LdifTest.java b/src/test/java/edu/vt/middleware/ldap/ldif/LdifTest.java
new file mode 100644
index 0000000..be107f5
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/ldif/LdifTest.java
@@ -0,0 +1,176 @@
+/*
+  $Id: LdifTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.ldif;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Iterator;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import edu.vt.middleware.ldap.bean.SortedLdapBeanFactory;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link Ldif}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class LdifTest
+{
+
+  /** Entry created for ldap tests. */
+  private static LdapEntry testLdapEntry;
+
+
+  /**
+   * @param  ldifFile  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry12" })
+  @BeforeClass(groups = {"ldiftest"})
+  public void createLdapEntry(final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"ldiftest"})
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  dn  to search on.
+   * @param  filter  to search with.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "ldifSearchDn",
+      "ldifSearchFilter"
+    })
+  @Test(groups = {"ldiftest"})
+  public void searchAndCompareLdif(final String dn, final String filter)
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createLdap();
+    final Ldif ldif = new Ldif();
+
+    final Iterator<SearchResult> iter = ldap.search(
+      dn,
+      new SearchFilter(filter));
+
+    final LdapResult result1 = TestUtil.newLdapResult(iter);
+    final StringWriter writer = new StringWriter();
+    ldif.outputLdif(result1.toSearchResults().iterator(), writer);
+
+    final StringReader reader = new StringReader(writer.toString());
+    final LdapResult result2 = TestUtil.newLdapResult(ldif.importLdif(reader));
+
+    AssertJUnit.assertEquals(result1, result2);
+    ldap.close();
+  }
+
+
+  /**
+   * @param  ldifFile  to create with
+   * @param  ldifSortedFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "ldifEntry",
+      "ldifSortedEntry"
+    })
+  @Test(groups = {"ldiftest"})
+  public void readAndCompareSortedLdif(
+    final String ldifFile,
+    final String ldifSortedFile)
+    throws Exception
+  {
+    final Ldif ldif = new Ldif();
+    ldif.setLdapBeanFactory(new SortedLdapBeanFactory());
+
+    final String ldifStringSorted = TestUtil.readFileIntoString(ldifSortedFile);
+    final Iterator<SearchResult> iter = ldif.importLdif(
+      new StringReader(TestUtil.readFileIntoString(ldifFile)));
+    final StringWriter writer = new StringWriter();
+    ldif.outputLdif(iter, writer);
+
+    AssertJUnit.assertEquals(ldifStringSorted, writer.toString());
+  }
+
+
+  /**
+   * @param  ldifFileIn  to create with
+   * @param  ldifFileOut  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "multipleLdifResultsIn",
+      "multipleLdifResultsOut"
+    })
+  @Test(groups = {"ldiftest"})
+  public void readAndCompareMultipleLdif(
+    final String ldifFileIn,
+    final String ldifFileOut)
+    throws Exception
+  {
+    final Ldif ldif = new Ldif();
+    final String ldifStringIn = TestUtil.readFileIntoString(ldifFileIn);
+    Iterator<SearchResult> iter = ldif.importLdif(
+      new StringReader(ldifStringIn));
+
+    final LdapResult ldif1 = TestUtil.newLdapResult(iter);
+
+    final String ldifStringOut = TestUtil.readFileIntoString(ldifFileOut);
+    iter = ldif.importLdif(new StringReader(ldifStringOut));
+
+    final LdapResult ldif2 = TestUtil.newLdapResult(iter);
+    AssertJUnit.assertEquals(ldif1, ldif2);
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/pool/LdapPoolTest.java b/src/test/java/edu/vt/middleware/ldap/pool/LdapPoolTest.java
new file mode 100644
index 0000000..d23a4e2
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/pool/LdapPoolTest.java
@@ -0,0 +1,1109 @@
+/*
+  $Id: LdapPoolTest.java 2218 2012-01-23 19:58:08Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2218 $
+  Updated: $Date: 2012-01-23 19:58:08 +0000 (Mon, 23 Jan 2012) $
+*/
+package edu.vt.middleware.ldap.pool;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import javax.naming.directory.SearchResult;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapConfig;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.handler.ConnectionHandler;
+import edu.vt.middleware.ldap.ldif.Ldif;
+import edu.vt.middleware.ldap.pool.commons.CommonsLdapPool;
+import edu.vt.middleware.ldap.pool.commons.DefaultLdapPoolableObjectFactory;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Load test for ldap pools.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2218 $
+ */
+public class LdapPoolTest
+{
+
+  /** Entries for pool tests. */
+  private static Map<String, LdapEntry[]> entries =
+    new HashMap<String, LdapEntry[]>();
+
+  /**
+   * Intialize the map of entries.
+   */
+  static {
+    for (int i = 2; i <= 10; i++) {
+      entries.put(String.valueOf(i), new LdapEntry[2]);
+    }
+  }
+
+  /** Log for this class. */
+  protected final Log logger = LogFactory.getLog(this.getClass());
+
+  /** LdapPool instance for concurrency testing. */
+  private SoftLimitLdapPool softLimitPool;
+
+  /** LdapPool instance for concurrency testing. */
+  private BlockingLdapPool blockingPool;
+
+  /** LdapPool instance for concurrency testing. */
+  private BlockingLdapPool blockingTimeoutPool;
+
+  /** LdapPool instance for concurrency testing. */
+  private SharedLdapPool sharedPool;
+
+  /** LdapPool instance for concurrency testing. */
+  private BlockingLdapPool connStrategyPool;
+
+  /** LdapPool instance for concurrency testing. */
+  private BlockingLdapPool vtComparisonPool;
+
+  /** Commons LdapPool for comparison testing. */
+  private CommonsLdapPool commonsComparisonPool;
+
+  /** Time in millis it takes the pool test to run. */
+  private long softLimitRuntime;
+
+  /** Time in millis it takes the pool test to run. */
+  private long blockingRuntime;
+
+  /** Time in millis it takes the pool test to run. */
+  private long blockingTimeoutRuntime;
+
+  /** Time in millis it takes the pool test to run. */
+  private long sharedRuntime;
+
+  /** Time in millis it takes the pool test to run. */
+  private long vtPoolRuntime;
+
+  /** Time in millis it takes the pool test to run. */
+  private long commonsPoolRuntime;
+
+
+  /**
+   * Default constructor.
+   *
+   * @throws  Exception  On test failure.
+   */
+  public LdapPoolTest()
+    throws Exception
+  {
+    final DefaultLdapFactory factory = new DefaultLdapFactory(
+      TestUtil.createLdap().getLdapConfig());
+    factory.setLdapValidator(
+      new CompareLdapValidator(
+        "ou=test,dc=vt,dc=edu",
+        new SearchFilter("ou=test")));
+
+    final LdapPoolConfig softLimitLpc = new LdapPoolConfig();
+    softLimitLpc.setValidateOnCheckIn(true);
+    softLimitLpc.setValidateOnCheckOut(true);
+    softLimitLpc.setValidatePeriodically(true);
+    softLimitLpc.setPruneTimerPeriod(5000L);
+    softLimitLpc.setExpirationTime(1000L);
+    softLimitLpc.setValidateTimerPeriod(5000L);
+    this.softLimitPool = new SoftLimitLdapPool(softLimitLpc, factory);
+
+    final LdapPoolConfig blockingLpc = new LdapPoolConfig();
+    blockingLpc.setValidateOnCheckIn(true);
+    blockingLpc.setValidateOnCheckOut(true);
+    blockingLpc.setValidatePeriodically(true);
+    blockingLpc.setPruneTimerPeriod(5000L);
+    blockingLpc.setExpirationTime(1000L);
+    blockingLpc.setValidateTimerPeriod(5000L);
+    this.blockingPool = new BlockingLdapPool(blockingLpc, factory);
+
+    final LdapPoolConfig blockingTimeoutLpc = new LdapPoolConfig();
+    blockingTimeoutLpc.setValidateOnCheckIn(true);
+    blockingTimeoutLpc.setValidateOnCheckOut(true);
+    blockingTimeoutLpc.setValidatePeriodically(true);
+    blockingTimeoutLpc.setPruneTimerPeriod(5000L);
+    blockingTimeoutLpc.setExpirationTime(1000L);
+    blockingTimeoutLpc.setValidateTimerPeriod(5000L);
+    this.blockingTimeoutPool = new BlockingLdapPool(blockingLpc, factory);
+    this.blockingTimeoutPool.setBlockWaitTime(1000L);
+
+    final LdapPoolConfig sharedLpc = new LdapPoolConfig();
+    sharedLpc.setValidateOnCheckIn(true);
+    sharedLpc.setValidateOnCheckOut(true);
+    sharedLpc.setValidatePeriodically(true);
+    sharedLpc.setPruneTimerPeriod(5000L);
+    sharedLpc.setExpirationTime(1000L);
+    sharedLpc.setValidateTimerPeriod(5000L);
+    this.sharedPool = new SharedLdapPool(sharedLpc, factory);
+
+    final LdapConfig connStrategyLc = TestUtil.createLdap().getLdapConfig();
+    connStrategyLc.setLdapUrl(
+      "ldap://ldap-test-1.middleware.vt.edu:10389 " +
+      "ldap://dne.middleware.vt.edu");
+    connStrategyLc.getConnectionHandler().setConnectionStrategy(
+      ConnectionHandler.ConnectionStrategy.ROUND_ROBIN);
+    final DefaultLdapFactory connStrategyFactory = new DefaultLdapFactory(
+      connStrategyLc);
+    this.connStrategyPool = new BlockingLdapPool(
+      new LdapPoolConfig(), connStrategyFactory);
+
+    // configure comparison pools
+    final LdapPoolConfig vtComparisonLpc = new LdapPoolConfig();
+    vtComparisonLpc.setValidateOnCheckIn(true);
+    vtComparisonLpc.setValidateOnCheckOut(true);
+    this.vtComparisonPool = new BlockingLdapPool(vtComparisonLpc, factory);
+
+    final DefaultLdapPoolableObjectFactory commonsFactory =
+      new DefaultLdapPoolableObjectFactory();
+    commonsFactory.setLdapValidator(
+      new CompareLdapValidator(
+        "ou=test,dc=vt,dc=edu",
+        new SearchFilter("ou=test")));
+    this.commonsComparisonPool = new CommonsLdapPool(commonsFactory);
+    this.commonsComparisonPool.setTestOnReturn(true);
+    this.commonsComparisonPool.setTestOnBorrow(true);
+  }
+
+
+  /**
+   * @param  ldifFile2  to create.
+   * @param  ldifFile3  to create.
+   * @param  ldifFile4  to create.
+   * @param  ldifFile5  to create.
+   * @param  ldifFile6  to create.
+   * @param  ldifFile7  to create.
+   * @param  ldifFile8  to create.
+   * @param  ldifFile9  to create.
+   * @param  ldifFile10  to create.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "createEntry2",
+      "createEntry3",
+      "createEntry4",
+      "createEntry5",
+      "createEntry6",
+      "createEntry7",
+      "createEntry8",
+      "createEntry9",
+      "createEntry10"
+    }
+  )
+  @BeforeClass(
+    groups = {
+      "queuepooltest",
+      "softlimitpooltest",
+      "blockingpooltest",
+      "blockingtimeoutpooltest",
+      "sharedpooltest",
+      "connstrategypooltest",
+      "comparisonpooltest"
+    }
+  )
+  public void createPoolEntry(
+    final String ldifFile2,
+    final String ldifFile3,
+    final String ldifFile4,
+    final String ldifFile5,
+    final String ldifFile6,
+    final String ldifFile7,
+    final String ldifFile8,
+    final String ldifFile9,
+    final String ldifFile10)
+    throws Exception
+  {
+    entries.get("2")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile2));
+    entries.get("3")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile3));
+    entries.get("4")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile4));
+    entries.get("5")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile5));
+    entries.get("6")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile6));
+    entries.get("7")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile7));
+    entries.get("8")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile8));
+    entries.get("9")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile9));
+    entries.get("10")[0] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile10));
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    for (Map.Entry<String, LdapEntry[]> e : entries.entrySet()) {
+      ldap.create(
+        e.getValue()[0].getDn(),
+        e.getValue()[0].getLdapAttributes().toAttributes());
+    }
+    ldap.close();
+
+    ldap = TestUtil.createLdap();
+    for (Map.Entry<String, LdapEntry[]> e : entries.entrySet()) {
+      while (
+        !ldap.compare(
+            e.getValue()[0].getDn(),
+            new SearchFilter(e.getValue()[0].getDn().split(",")[0]))) {
+        Thread.sleep(100);
+      }
+    }
+    ldap.close();
+
+    this.softLimitPool.initialize();
+    this.blockingPool.initialize();
+    this.blockingTimeoutPool.initialize();
+    this.sharedPool.initialize();
+    this.connStrategyPool.initialize();
+  }
+
+
+  /**
+   * @param  ldifFile2  to load.
+   * @param  ldifFile3  to load.
+   * @param  ldifFile4  to load.
+   * @param  ldifFile5  to load.
+   * @param  ldifFile6  to load.
+   * @param  ldifFile7  to load.
+   * @param  ldifFile8  to load.
+   * @param  ldifFile9  to load.
+   * @param  ldifFile10  to load.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "searchResults2",
+      "searchResults3",
+      "searchResults4",
+      "searchResults5",
+      "searchResults6",
+      "searchResults7",
+      "searchResults8",
+      "searchResults9",
+      "searchResults10"
+    }
+  )
+  @BeforeClass(
+    groups = {
+      "queuepooltest",
+      "softlimitpooltest",
+      "blockingpooltest",
+      "blockingtimeoutpooltest",
+      "sharedpooltest",
+      "connstrategypooltest",
+      "comparisonpooltest"
+    }
+  )
+  public void loadPoolSearchResults(
+    final String ldifFile2,
+    final String ldifFile3,
+    final String ldifFile4,
+    final String ldifFile5,
+    final String ldifFile6,
+    final String ldifFile7,
+    final String ldifFile8,
+    final String ldifFile9,
+    final String ldifFile10)
+    throws Exception
+  {
+    entries.get("2")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile2));
+    entries.get("3")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile3));
+    entries.get("4")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile4));
+    entries.get("5")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile5));
+    entries.get("6")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile6));
+    entries.get("7")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile7));
+    entries.get("8")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile8));
+    entries.get("9")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile9));
+    entries.get("10")[1] = TestUtil.convertLdifToEntry(
+      TestUtil.readFileIntoString(ldifFile10));
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(
+    groups = {
+      "queuepooltest",
+      "softlimitpooltest",
+      "blockingpooltest",
+      "blockingtimeoutpooltest",
+      "sharedpooltest",
+      "connstrategypooltest",
+      "comparisonpooltest"
+    }
+  )
+  public void deletePoolEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(entries.get("2")[0].getDn());
+    ldap.delete(entries.get("3")[0].getDn());
+    ldap.delete(entries.get("4")[0].getDn());
+    ldap.delete(entries.get("5")[0].getDn());
+    ldap.delete(entries.get("6")[0].getDn());
+    ldap.delete(entries.get("7")[0].getDn());
+    ldap.delete(entries.get("8")[0].getDn());
+    ldap.delete(entries.get("9")[0].getDn());
+    ldap.delete(entries.get("10")[0].getDn());
+    ldap.close();
+
+    this.softLimitPool.close();
+    AssertJUnit.assertEquals(this.softLimitPool.availableCount(), 0);
+    AssertJUnit.assertEquals(this.softLimitPool.activeCount(), 0);
+    this.blockingPool.close();
+    AssertJUnit.assertEquals(this.blockingPool.availableCount(), 0);
+    AssertJUnit.assertEquals(this.blockingPool.activeCount(), 0);
+    this.blockingTimeoutPool.close();
+    AssertJUnit.assertEquals(this.blockingTimeoutPool.availableCount(), 0);
+    AssertJUnit.assertEquals(this.blockingTimeoutPool.activeCount(), 0);
+    this.sharedPool.close();
+    AssertJUnit.assertEquals(this.sharedPool.availableCount(), 0);
+    AssertJUnit.assertEquals(this.sharedPool.activeCount(), 0);
+    this.connStrategyPool.close();
+    AssertJUnit.assertEquals(this.connStrategyPool.availableCount(), 0);
+    AssertJUnit.assertEquals(this.connStrategyPool.activeCount(), 0);
+    this.vtComparisonPool.close();
+    AssertJUnit.assertEquals(this.vtComparisonPool.availableCount(), 0);
+    AssertJUnit.assertEquals(this.vtComparisonPool.activeCount(), 0);
+    this.commonsComparisonPool.clear();
+    this.commonsComparisonPool.close();
+    AssertJUnit.assertEquals(this.commonsComparisonPool.getNumActive(), 0);
+    AssertJUnit.assertEquals(this.commonsComparisonPool.getNumIdle(), 0);
+    // vt pool should be minimally faster
+    AssertJUnit.assertEquals(
+      this.vtPoolRuntime,
+      Math.min(this.vtPoolRuntime, this.commonsPoolRuntime));
+  }
+
+
+  /**
+   * Sample user data.
+   *
+   * @return  user data
+   */
+  @DataProvider(name = "pool-data")
+  public Object[][] createPoolData()
+  {
+    return
+      new Object[][] {
+        {
+          new SearchFilter("mail=jdoe2 at vt.edu"),
+          "departmentNumber|givenName|sn",
+          entries.get("2")[1],
+        },
+        {
+          new SearchFilter("mail=jdoe3 at vt.edu"),
+          "departmentNumber|givenName|sn",
+          entries.get("3")[1],
+        },
+        {
+          new SearchFilter("mail=jdoe4 at vt.edu"),
+          "departmentNumber|givenName|sn",
+          entries.get("4")[1],
+        },
+        {
+          new SearchFilter("mail=jdoe5 at vt.edu"),
+          "departmentNumber|givenName|sn",
+          entries.get("5")[1],
+        },
+        {
+          new SearchFilter("mail=jdoe6 at vt.edu"),
+          "departmentNumber|givenName|sn",
+          entries.get("6")[1],
+        },
+        {
+          new SearchFilter("mail=jdoe7 at vt.edu"),
+          "departmentNumber|givenName|sn",
+          entries.get("7")[1],
+        },
+        {
+          new SearchFilter("mail=jdoe8 at vt.edu"),
+          "departmentNumber|givenName|sn|jpegPhoto",
+          entries.get("8")[1],
+        },
+        {
+          new SearchFilter("mail=jdoe9 at vt.edu"),
+          "departmentNumber|givenName|sn",
+          entries.get("9")[1],
+        },
+        {
+          new SearchFilter("mail=jdoe10 at vt.edu"),
+          "departmentNumber|givenName|sn",
+          entries.get("10")[1],
+        },
+      };
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"softlimitpooltest"})
+  public void checkSoftLimitPoolImmutable()
+    throws Exception
+  {
+    try {
+      this.softLimitPool.getLdapPoolConfig().setMinPoolSize(8);
+      AssertJUnit.fail("Expected illegalstateexception to be thrown");
+    } catch (IllegalStateException e) {
+      AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+    }
+
+    Ldap ldap = null;
+    try {
+      ldap = this.softLimitPool.checkOut();
+      try {
+        ldap.setLdapConfig(new LdapConfig());
+        AssertJUnit.fail("Expected illegalstateexception to be thrown");
+      } catch (IllegalStateException e) {
+        AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+      }
+      try {
+        ldap.getLdapConfig().setTimeout(10000);
+        AssertJUnit.fail("Expected illegalstateexception to be thrown");
+      } catch (IllegalStateException e) {
+        AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+      }
+    } finally {
+      this.softLimitPool.checkIn(ldap);
+    }
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "softlimitpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 3,
+    invocationCount = 50,
+    timeOut = 60000
+  )
+  public void softLimitSmallSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.softLimitRuntime += this.search(
+      this.softLimitPool,
+      filter,
+      returnAttrs,
+      results);
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "softlimitpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000,
+    dependsOnMethods = {"softLimitSmallSearch"}
+  )
+  public void softLimitMediumSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.softLimitRuntime += this.search(
+      this.softLimitPool,
+      filter,
+      returnAttrs,
+      results);
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(
+    groups = {"queuepooltest", "softlimitpooltest"},
+    dependsOnMethods = {"softLimitMediumSearch"}
+  )
+  public void softLimitMaxClean()
+    throws Exception
+  {
+    Thread.sleep(10000);
+    AssertJUnit.assertEquals(0, this.softLimitPool.activeCount());
+    AssertJUnit.assertEquals(
+      LdapPoolConfig.DEFAULT_MIN_POOL_SIZE,
+      this.softLimitPool.availableCount());
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"blockingpooltest"})
+  public void checkBlockingPoolImmutable()
+    throws Exception
+  {
+    try {
+      this.blockingPool.getLdapPoolConfig().setMinPoolSize(8);
+      AssertJUnit.fail("Expected illegalstateexception to be thrown");
+    } catch (IllegalStateException e) {
+      AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+    }
+
+    Ldap ldap = null;
+    try {
+      ldap = this.blockingPool.checkOut();
+      try {
+        ldap.setLdapConfig(new LdapConfig());
+        AssertJUnit.fail("Expected illegalstateexception to be thrown");
+      } catch (IllegalStateException e) {
+        AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+      }
+      try {
+        ldap.getLdapConfig().setTimeout(10000);
+        AssertJUnit.fail("Expected illegalstateexception to be thrown");
+      } catch (IllegalStateException e) {
+        AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+      }
+    } finally {
+      this.blockingPool.checkIn(ldap);
+    }
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "blockingpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 3,
+    invocationCount = 50,
+    timeOut = 60000
+  )
+  public void blockingSmallSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.blockingRuntime += this.search(
+      this.blockingPool,
+      filter,
+      returnAttrs,
+      results);
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "blockingpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000,
+    dependsOnMethods = {"blockingSmallSearch"}
+  )
+  public void blockingMediumSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.blockingRuntime += this.search(
+      this.blockingPool,
+      filter,
+      returnAttrs,
+      results);
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "blockingpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 50,
+    invocationCount = 1000,
+    timeOut = 60000,
+    dependsOnMethods = {"blockingMediumSearch"}
+  )
+  public void blockingLargeSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.blockingRuntime += this.search(
+      this.blockingPool,
+      filter,
+      returnAttrs,
+      results);
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(
+    groups = {"queuepooltest", "blockingpooltest"},
+    dependsOnMethods = {"blockingLargeSearch"}
+  )
+  public void blockingMaxClean()
+    throws Exception
+  {
+    Thread.sleep(10000);
+    AssertJUnit.assertEquals(0, this.blockingPool.activeCount());
+    AssertJUnit.assertEquals(
+      LdapPoolConfig.DEFAULT_MIN_POOL_SIZE,
+      this.blockingPool.availableCount());
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "blockingtimeoutpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 3,
+    invocationCount = 50,
+    timeOut = 60000
+  )
+  public void blockingTimeoutSmallSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    try {
+      this.blockingTimeoutRuntime += this.search(
+        this.blockingTimeoutPool,
+        filter,
+        returnAttrs,
+        results);
+    } catch (BlockingTimeoutException e) {
+      this.logger.info("block timeout exceeded");
+    }
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "blockingtimeoutpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000,
+    dependsOnMethods = {"blockingTimeoutSmallSearch"}
+  )
+  public void blockingTimeoutMediumSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    try {
+      this.blockingTimeoutRuntime += this.search(
+        this.blockingTimeoutPool,
+        filter,
+        returnAttrs,
+        results);
+    } catch (BlockingTimeoutException e) {
+      this.logger.info("block timeout exceeded");
+    }
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "blockingtimeoutpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 50,
+    invocationCount = 1000,
+    timeOut = 60000,
+    dependsOnMethods = {"blockingTimeoutMediumSearch"}
+  )
+  public void blockingTimeoutLargeSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    try {
+      this.blockingTimeoutRuntime += this.search(
+        this.blockingTimeoutPool,
+        filter,
+        returnAttrs,
+        results);
+    } catch (BlockingTimeoutException e) {
+      this.logger.info("block timeout exceeded");
+    }
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(
+    groups = {"queuepooltest", "blockingtimeoutpooltest"},
+    dependsOnMethods = {"blockingTimeoutLargeSearch"}
+  )
+  public void blockingTimeoutMaxClean()
+    throws Exception
+  {
+    Thread.sleep(10000);
+    AssertJUnit.assertEquals(0, this.blockingTimeoutPool.activeCount());
+    AssertJUnit.assertEquals(
+      LdapPoolConfig.DEFAULT_MIN_POOL_SIZE,
+      this.blockingTimeoutPool.availableCount());
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"sharedpooltest"})
+  public void checkSharedPoolImmutable()
+    throws Exception
+  {
+    try {
+      this.sharedPool.getLdapPoolConfig().setMinPoolSize(8);
+      AssertJUnit.fail("Expected illegalstateexception to be thrown");
+    } catch (IllegalStateException e) {
+      AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+    }
+
+    Ldap ldap = null;
+    try {
+      ldap = this.sharedPool.checkOut();
+      try {
+        ldap.setLdapConfig(new LdapConfig());
+        AssertJUnit.fail("Expected illegalstateexception to be thrown");
+      } catch (IllegalStateException e) {
+        AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+      }
+      try {
+        ldap.getLdapConfig().setTimeout(10000);
+        AssertJUnit.fail("Expected illegalstateexception to be thrown");
+      } catch (IllegalStateException e) {
+        AssertJUnit.assertEquals(IllegalStateException.class, e.getClass());
+      }
+    } finally {
+      this.sharedPool.checkIn(ldap);
+    }
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "sharedpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 3,
+    invocationCount = 50,
+    timeOut = 60000
+  )
+  public void sharedSmallSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.sharedRuntime += this.search(
+      this.sharedPool,
+      filter,
+      returnAttrs,
+      results);
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "sharedpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000,
+    dependsOnMethods = {"sharedSmallSearch"}
+  )
+  public void sharedMediumSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.sharedRuntime += this.search(
+      this.sharedPool,
+      filter,
+      returnAttrs,
+      results);
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "sharedpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 50,
+    invocationCount = 1000,
+    timeOut = 60000,
+    dependsOnMethods = {"sharedMediumSearch"}
+  )
+  public void sharedLargeSearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.sharedRuntime += this.search(
+      this.sharedPool,
+      filter,
+      returnAttrs,
+      results);
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(
+    groups = {"queuepooltest", "sharedpooltest"},
+    dependsOnMethods = {"sharedLargeSearch"}
+  )
+  public void sharedMaxClean()
+    throws Exception
+  {
+    Thread.sleep(10000);
+    AssertJUnit.assertEquals(0, this.sharedPool.activeCount());
+    AssertJUnit.assertEquals(
+      LdapPoolConfig.DEFAULT_MIN_POOL_SIZE,
+      this.sharedPool.availableCount());
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"queuepooltest", "connstrategypooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 10,
+    invocationCount = 100,
+    timeOut = 60000
+  )
+  public void connStrategySearch(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    this.search(this.connStrategyPool, filter, returnAttrs, results);
+  }
+
+
+  /**
+   * @param  pool  to get ldap object from.
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @return  time it takes to checkout/search/checkin from the pool
+   *
+   * @throws  Exception  On test failure.
+   */
+  private long search(
+    final LdapPool<Ldap> pool,
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    final long startTime = System.currentTimeMillis();
+    Ldap ldap = null;
+    Iterator<SearchResult> iter = null;
+    try {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("waiting for pool checkout");
+      }
+      ldap = pool.checkOut();
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("performing search");
+      }
+      iter = ldap.search(filter, returnAttrs.split("\\|"));
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("search completed");
+      }
+    } finally {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("returning ldap to pool");
+      }
+      pool.checkIn(ldap);
+    }
+    AssertJUnit.assertEquals(
+      results,
+      TestUtil.convertLdifToEntry((new Ldif()).createLdif(iter)));
+    return System.currentTimeMillis() - startTime;
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"comparisonpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 50,
+    invocationCount = 1000,
+    timeOut = 60000
+  )
+  public void vtPoolComparison(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    final long startTime = System.currentTimeMillis();
+    Ldap ldap = null;
+    try {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("waiting for pool checkout");
+      }
+      ldap = this.vtComparisonPool.checkOut();
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("performing search");
+      }
+      ldap.search(filter, returnAttrs.split("\\|"));
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("search completed");
+      }
+    } finally {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("returning ldap to pool");
+      }
+      this.vtComparisonPool.checkIn(ldap);
+    }
+    this.vtPoolRuntime += System.currentTimeMillis() - startTime;
+  }
+
+
+  /**
+   * @param  filter  to search with.
+   * @param  returnAttrs  to search for.
+   * @param  results  to expect from the search.
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(
+    groups = {"comparisonpooltest"},
+    dataProvider = "pool-data",
+    threadPoolSize = 50,
+    invocationCount = 1000,
+    timeOut = 60000
+  )
+  public void commonsPoolComparison(
+    final SearchFilter filter,
+    final String returnAttrs,
+    final LdapEntry results)
+    throws Exception
+  {
+    final long startTime = System.currentTimeMillis();
+    Ldap ldap = null;
+    try {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("waiting for pool checkout");
+      }
+      ldap = (Ldap) this.commonsComparisonPool.borrowObject();
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("performing search");
+      }
+      ldap.search(filter, returnAttrs.split("\\|"));
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("search completed");
+      }
+    } finally {
+      if (this.logger.isTraceEnabled()) {
+        this.logger.trace("returning ldap to pool");
+      }
+      this.commonsComparisonPool.returnObject(ldap);
+    }
+    this.commonsPoolRuntime += System.currentTimeMillis() - startTime;
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/pool/commons/CommonsLdapPool.java b/src/test/java/edu/vt/middleware/ldap/pool/commons/CommonsLdapPool.java
new file mode 100644
index 0000000..1258171
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/pool/commons/CommonsLdapPool.java
@@ -0,0 +1,46 @@
+/*
+  $Id: CommonsLdapPool.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool.commons;
+
+import org.apache.commons.pool.PoolableObjectFactory;
+import org.apache.commons.pool.impl.GenericObjectPool;
+
+/**
+ * <code>CommonsLdapPool</code> provides a implementation of a commons pooling
+ * <code>GenericObjectPool</code> for testing.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class CommonsLdapPool extends GenericObjectPool
+{
+
+
+  /** Creates a new ldap pool using {@link DefaultLdapPoolableObjectFactory}. */
+  public CommonsLdapPool()
+  {
+    this(new DefaultLdapPoolableObjectFactory());
+  }
+
+
+  /**
+   * Creates a new ldap pool using the supplied poolable object factory.
+   *
+   * @param  poolableObjectFactory  to create Ldap objects with
+   */
+  public CommonsLdapPool(final PoolableObjectFactory poolableObjectFactory)
+  {
+    super(poolableObjectFactory);
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/pool/commons/DefaultLdapPoolableObjectFactory.java b/src/test/java/edu/vt/middleware/ldap/pool/commons/DefaultLdapPoolableObjectFactory.java
new file mode 100644
index 0000000..33319a0
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/pool/commons/DefaultLdapPoolableObjectFactory.java
@@ -0,0 +1,64 @@
+/*
+  $Id: DefaultLdapPoolableObjectFactory.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.pool.commons;
+
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.pool.DefaultLdapFactory;
+import org.apache.commons.pool.PoolableObjectFactory;
+
+/**
+ * <code>DefaultLdapPoolableObjectFactory</code> provides a implementation of a
+ * commons pooling <code>PoolableObjectFactory</code> for testing.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class DefaultLdapPoolableObjectFactory extends DefaultLdapFactory
+  implements PoolableObjectFactory
+{
+
+  /** {@inheritDoc} */
+  public void activateObject(final Object obj)
+  {
+    this.activate((Ldap) obj);
+  }
+
+
+  /** {@inheritDoc} */
+  public void destroyObject(final Object obj)
+  {
+    this.destroy((Ldap) obj);
+  }
+
+
+  /** {@inheritDoc} */
+  public Object makeObject()
+  {
+    return this.create();
+  }
+
+
+  /** {@inheritDoc} */
+  public void passivateObject(final Object obj)
+  {
+    this.passivate((Ldap) obj);
+  }
+
+
+  /** {@inheritDoc} */
+  public boolean validateObject(final Object obj)
+  {
+    return this.validate((Ldap) obj);
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/servlets/AttributeServletTest.java b/src/test/java/edu/vt/middleware/ldap/servlets/AttributeServletTest.java
new file mode 100644
index 0000000..fc1ce15
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/servlets/AttributeServletTest.java
@@ -0,0 +1,161 @@
+/*
+  $Id: AttributeServletTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import com.meterware.httpunit.PostMethodWebRequest;
+import com.meterware.httpunit.WebRequest;
+import com.meterware.httpunit.WebResponse;
+import com.meterware.servletunit.ServletRunner;
+import com.meterware.servletunit.ServletUnitClient;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.LdapUtil;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link AttributeServlet}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class AttributeServletTest
+{
+
+  /** Entry created for tests. */
+  private static LdapEntry testLdapEntry;
+
+  /** To test servlets with. */
+  private ServletRunner servletRunner;
+
+
+  /**
+   * @param  ldifFile  to create.
+   * @param  webXml  web.xml for queries
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry9", "webXml" })
+  @BeforeClass(groups = {"servlettest"})
+  public void createLdapEntry(final String ldifFile, final String webXml)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+
+    this.servletRunner = new ServletRunner(new File(webXml));
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"servlettest"})
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  query  to search for.
+   * @param  attr  attribute to return from search
+   * @param  attributeValue  to compare
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "attributeServletQuery",
+      "attributeServletAttr",
+      "attributeServletValue"
+    }
+  )
+  @Test(groups = {"servlettest"})
+  public void attributeServlet(
+    final String query,
+    final String attr,
+    final String attributeValue)
+    throws Exception
+  {
+    final ServletUnitClient sc = this.servletRunner.newClient();
+    final WebRequest request = new PostMethodWebRequest(
+      "http://servlets.ldap.middleware.vt.edu/AttributeSearch");
+    request.setParameter("query", query);
+    request.setParameter("attr", attr);
+    request.setParameter("content-type", "octet");
+
+    final WebResponse response = sc.getResponse(request);
+
+    AssertJUnit.assertNotNull(response);
+    AssertJUnit.assertEquals(
+      "application/octet-stream",
+      response.getContentType());
+    AssertJUnit.assertEquals(
+      "attachment; filename=\"" + attr + ".bin\"",
+      response.getHeaderField("Content-Disposition"));
+
+    final InputStream input = response.getInputStream();
+    final ByteArrayOutputStream data = new ByteArrayOutputStream();
+    if (input != null) {
+      try {
+        final byte[] buffer = new byte[128];
+        int length;
+        while ((length = input.read(buffer)) != -1) {
+          data.write(buffer, 0, length);
+        }
+      } finally {
+        data.close();
+      }
+    }
+    AssertJUnit.assertEquals(
+      attributeValue,
+      LdapUtil.base64Encode(data.toByteArray()));
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(
+    groups = {"servlettest"},
+    dependsOnMethods = {"attributeServlet"}
+  )
+  public void prunePools()
+    throws Exception
+  {
+    Thread.sleep(10000);
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/servlets/SearchServletTest.java b/src/test/java/edu/vt/middleware/ldap/servlets/SearchServletTest.java
new file mode 100644
index 0000000..7c9d27a
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/servlets/SearchServletTest.java
@@ -0,0 +1,211 @@
+/*
+  $Id: SearchServletTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+import java.io.File;
+import com.meterware.httpunit.PostMethodWebRequest;
+import com.meterware.httpunit.WebRequest;
+import com.meterware.httpunit.WebResponse;
+import com.meterware.servletunit.ServletRunner;
+import com.meterware.servletunit.ServletUnitClient;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import edu.vt.middleware.ldap.bean.LdapResult;
+import edu.vt.middleware.ldap.dsml.DsmlResultConverter;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link SearchServlet}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class SearchServletTest
+{
+
+  /** Entry created for tests. */
+  private static LdapEntry testLdapEntry;
+
+  /** To test servlets with. */
+  private ServletRunner ldifServletRunner;
+
+  /** To test servlets with. */
+  private ServletRunner dsmlServletRunner;
+
+
+  /**
+   * @param  ldifFile  to create.
+   * @param  webXml  web.xml for queries
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry8", "webXml" })
+  @BeforeClass(groups = {"servlettest"})
+  public void createLdapEntry(final String ldifFile, final String webXml)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+
+    this.ldifServletRunner = new ServletRunner(new File(webXml));
+    this.dsmlServletRunner = new ServletRunner(new File(webXml));
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"servlettest"})
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  query  to search for.
+   * @param  attrs  attributes to return from search
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "ldifSearchServletQuery",
+      "ldifSearchServletAttrs",
+      "ldifSearchServletLdif"
+    }
+  )
+  @Test(groups = {"servlettest"})
+  public void ldifSearchServlet(
+    final String query,
+    final String attrs,
+    final String ldifFile)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    final LdapEntry entry = TestUtil.convertLdifToEntry(ldif);
+
+    final ServletUnitClient sc = this.ldifServletRunner.newClient();
+    final WebRequest request = new PostMethodWebRequest(
+      "http://servlets.ldap.middleware.vt.edu/LdifSearch");
+    request.setParameter("query", query);
+    request.setParameter("attrs", attrs.split("\\|"));
+
+    final WebResponse response = sc.getResponse(request);
+
+    AssertJUnit.assertNotNull(response);
+    AssertJUnit.assertEquals("text/plain", response.getContentType());
+
+    final LdapEntry result = TestUtil.convertLdifToEntry(response.getText());
+    AssertJUnit.assertEquals(entry, result);
+  }
+
+
+  /**
+   * @param  query  to search for.
+   * @param  attrs  attributes to return from search
+   * @param  ldifFile  to compare with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters(
+    {
+      "dsmlSearchServletQuery",
+      "dsmlSearchServletAttrs",
+      "dsmlSearchServletLdif"
+    }
+  )
+  @Test(groups = {"servlettest"})
+  public void dsmlSearchServlet(
+    final String query,
+    final String attrs,
+    final String ldifFile)
+    throws Exception
+  {
+    final DsmlResultConverter converter = new DsmlResultConverter();
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    final LdapResult result = TestUtil.convertLdifToResult(ldif);
+
+    final ServletUnitClient sc = this.dsmlServletRunner.newClient();
+    // test basic dsml query
+    WebRequest request = new PostMethodWebRequest(
+      "http://servlets.ldap.middleware.vt.edu/DsmlSearch");
+    request.setParameter("query", query);
+    request.setParameter("attrs", attrs.split("\\|"));
+
+    WebResponse response = sc.getResponse(request);
+
+    AssertJUnit.assertNotNull(response);
+    AssertJUnit.assertEquals("text/xml", response.getContentType());
+    AssertJUnit.assertEquals(converter.toDsmlv1(result), response.getText());
+
+    // test plain text
+    request = new PostMethodWebRequest(
+      "http://servlets.ldap.middleware.vt.edu/DsmlSearch");
+    request.setParameter("content-type", "text");
+    request.setParameter("query", query);
+    request.setParameter("attrs", attrs.split("\\|"));
+    response = sc.getResponse(request);
+
+    AssertJUnit.assertNotNull(response);
+    AssertJUnit.assertEquals("text/plain", response.getContentType());
+    AssertJUnit.assertEquals(converter.toDsmlv1(result), response.getText());
+
+    // test dsmlv2
+    request = new PostMethodWebRequest(
+      "http://servlets.ldap.middleware.vt.edu/DsmlSearch");
+    request.setParameter("dsml-version", "2");
+    request.setParameter("query", query);
+    request.setParameter("attrs", attrs.split("\\|"));
+    response = sc.getResponse(request);
+
+    AssertJUnit.assertNotNull(response);
+    AssertJUnit.assertEquals("text/xml", response.getContentType());
+    AssertJUnit.assertEquals(converter.toDsmlv2(result), response.getText());
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(
+    groups = {"servlettest"},
+    dependsOnMethods = {"ldifSearchServlet", "dsmlSearchServlet"}
+  )
+  public void prunePools()
+    throws Exception
+  {
+    Thread.sleep(10000);
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/servlets/SessionCheck.java b/src/test/java/edu/vt/middleware/ldap/servlets/SessionCheck.java
new file mode 100644
index 0000000..459981f
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/servlets/SessionCheck.java
@@ -0,0 +1,60 @@
+/*
+  $Id: SessionCheck.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Enumeration;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * <code>SessionCheck</code> prints sessions variables for testing purposes.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $ $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+ */
+public class SessionCheck extends HttpServlet
+{
+
+  /** serial version uid. */
+  private static final long serialVersionUID = 2862964801686577549L;
+
+
+  /**
+   * Handle all requests sent to this servlet.
+   *
+   * @param  request  <code>HttpServletRequest</code>
+   * @param  response  <code>HttpServletResponse</code>
+   *
+   * @throws  ServletException  if this request cannot be serviced
+   * @throws  IOException  if a response cannot be sent
+   */
+  public void service(
+    final HttpServletRequest request,
+    final HttpServletResponse response)
+    throws ServletException, IOException
+  {
+    final PrintWriter out = response.getWriter();
+    final HttpSession session = request.getSession();
+    final Enumeration<?> e = session.getAttributeNames();
+    while (e.hasMoreElements()) {
+      final String k = (String) e.nextElement();
+      out.println(k + ":" + session.getAttribute(k));
+    }
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/servlets/session/SessionManagerTest.java b/src/test/java/edu/vt/middleware/ldap/servlets/session/SessionManagerTest.java
new file mode 100644
index 0000000..e8428db
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/servlets/session/SessionManagerTest.java
@@ -0,0 +1,144 @@
+/*
+  $Id: SessionManagerTest.java 1330 2010-05-23 22:10:53Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1330 $
+  Updated: $Date: 2010-05-23 23:10:53 +0100 (Sun, 23 May 2010) $
+*/
+package edu.vt.middleware.ldap.servlets.session;
+
+import java.io.File;
+import com.meterware.httpunit.GetMethodWebRequest;
+import com.meterware.httpunit.PostMethodWebRequest;
+import com.meterware.httpunit.WebRequest;
+import com.meterware.httpunit.WebResponse;
+import com.meterware.servletunit.ServletRunner;
+import com.meterware.servletunit.ServletUnitClient;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.SearchFilter;
+import edu.vt.middleware.ldap.TestUtil;
+import edu.vt.middleware.ldap.bean.LdapEntry;
+import org.testng.AssertJUnit;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Parameters;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link SessionManager}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1330 $
+ */
+public class SessionManagerTest
+{
+
+  /** Entry created for tests. */
+  private static LdapEntry testLdapEntry;
+
+  /** To test servlets with. */
+  private ServletRunner servletRunner;
+
+
+  /**
+   * @param  ldifFile  to create.
+   * @param  webXml  web.xml for queries
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({ "createEntry10", "webXml" })
+  @BeforeClass(groups = {"servlettest"})
+  public void createLdapEntry(final String ldifFile, final String webXml)
+    throws Exception
+  {
+    final String ldif = TestUtil.readFileIntoString(ldifFile);
+    testLdapEntry = TestUtil.convertLdifToEntry(ldif);
+
+    Ldap ldap = TestUtil.createSetupLdap();
+    ldap.create(
+      testLdapEntry.getDn(),
+      testLdapEntry.getLdapAttributes().toAttributes());
+    ldap.close();
+    ldap = TestUtil.createLdap();
+    while (
+      !ldap.compare(
+          testLdapEntry.getDn(),
+          new SearchFilter(testLdapEntry.getDn().split(",")[0]))) {
+      Thread.sleep(100);
+    }
+    ldap.close();
+
+    this.servletRunner = new ServletRunner(new File(webXml));
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @AfterClass(groups = {"servlettest"})
+  public void deleteLdapEntry()
+    throws Exception
+  {
+    final Ldap ldap = TestUtil.createSetupLdap();
+    ldap.delete(testLdapEntry.getDn());
+    ldap.close();
+  }
+
+
+  /**
+   * @param  user  to authenticate
+   * @param  password  to authenticate with
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Parameters({
+      "sessionManagerUser",
+      "sessionManagerPassword"
+    })
+  @Test(groups = {"servlettest"})
+  public void login(final String user, final String password)
+    throws Exception
+  {
+    System.setProperty(
+      "javax.net.ssl.trustStore",
+      "src/test/resources/ed.truststore");
+    System.setProperty("javax.net.ssl.trustStoreType", "BKS");
+    System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
+
+    final ServletUnitClient sc = this.servletRunner.newClient();
+    // login
+    WebRequest request = new PostMethodWebRequest(
+      "http://servlets.ldap.middleware.vt.edu/Login");
+    request.setParameter("user", user);
+    request.setParameter("credential", password);
+    request.setParameter(
+      "url",
+      "http://servlets.ldap.middleware.vt.edu/SessionCheck");
+
+    WebResponse response = sc.getResponse(request);
+
+    AssertJUnit.assertNotNull(response);
+
+    final String[] sessionData = response.getText().trim().split(":");
+    AssertJUnit.assertEquals(user, sessionData[1]);
+
+    // logout
+    request = new GetMethodWebRequest(
+      "http://servlets.ldap.middleware.vt.edu/Logout");
+    request.setParameter(
+      "url",
+      "http://servlets.ldap.middleware.vt.edu/SessionCheck");
+    response = sc.getResponse(request);
+
+    AssertJUnit.assertNotNull(response);
+    AssertJUnit.assertEquals("", response.getText());
+
+    System.clearProperty("javax.net.ssl.trustStore");
+    System.clearProperty("javax.net.ssl.trustStoreType");
+    System.clearProperty("javax.net.ssl.trustStorePassword");
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/ssl/DefaultHostnameVerifierTest.java b/src/test/java/edu/vt/middleware/ldap/ssl/DefaultHostnameVerifierTest.java
new file mode 100644
index 0000000..e50fa91
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/ssl/DefaultHostnameVerifierTest.java
@@ -0,0 +1,344 @@
+/*
+  $Id: DefaultHostnameVerifierTest.java 2217 2012-01-23 19:56:35Z dfisher $
+
+  Copyright (C) 2003-2012 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2217 $
+  Updated: $Date: 2012-01-23 19:56:35 +0000 (Mon, 23 Jan 2012) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import edu.vt.middleware.ldap.LdapUtil;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link DefaultHostnameVerifier}.
+ * Generate key with: openssl genrsa -aes256 -out test.key 2048
+ * Generate cert with:
+ * openssl req -new -x509 -sha1 -days 3650 -key test.key -out test.crt \
+ *   -subj "/CN=a.foo.com/DC=ldaptive/DC=org" -config openssl.cnf \
+ *   -extensions my_ext
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2217 $
+ */
+public class DefaultHostnameVerifierTest
+{
+
+  /** Instance of the default hostname verifier. */
+  private static final DefaultHostnameVerifier DEFAULT_VERIFIER =
+    new DefaultHostnameVerifier();
+
+  /** Instance of the default startTLS hostname verifier. */
+  private static final SunTLSHostnameVerifier SUN_VERIFIER =
+    new SunTLSHostnameVerifier();
+
+  /** Certificate with CN=a.foo.com. */
+  private static final String A_FOO_COM_CERT =
+    "MIIDrzCCApegAwIBAgIJAK+nL4I3GkjeMA0GCSqGSIb3DQEBBQUAMEMxEjAQBgNV" +
+    "BAMTCWEuZm9vLmNvbTEYMBYGCgmSJomT8ixkARkWCGxkYXB0aXZlMRMwEQYKCZIm" +
+    "iZPyLGQBGRYDb3JnMB4XDTEyMDExNzIxNDAxNVoXDTIyMDExNDIxNDAxNVowQzES" +
+    "MBAGA1UEAxMJYS5mb28uY29tMRgwFgYKCZImiZPyLGQBGRYIbGRhcHRpdmUxEzAR" +
+    "BgoJkiaJk/IsZAEZFgNvcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB" +
+    "AQDGRxBVvGZqHFWbYdbpOZaBf4H68b7zjiqbpXXq+mTfVOehIUeyL0624JsdmHLx" +
+    "oMNC7K9hAaM88wxcyhBRLRfo4ar1DJspzcFUoz2kFD7ytWGS2zwvV+VWnoXpPNiw" +
+    "9QuK6bdA/UYLIg/fk3TwshuoIT9VBJ4L3TRdOYYgH6WJBerQ2L5vMu91B9nBhNqR" +
+    "4RG8VFqwgwW9IoXBXC8XTZS5jd0bVoEoeA+PWVENQ3my5ilP4VUqo9h/jPdb8dFW" +
+    "3TNoaVHjOiTUOIpH+5cUmi0OkH2NzhTaWmCVoWuzFpvvB6PFHHxut2pDe8eGgc4x" +
+    "NdEvZDizbfY6JEb/fkwZ+Im9AgMBAAGjgaUwgaIwHQYDVR0OBBYEFPUscUXspD8Z" +
+    "LP3b6yybVhXhp5C2MHMGA1UdIwRsMGqAFPUscUXspD8ZLP3b6yybVhXhp5C2oUek" +
+    "RTBDMRIwEAYDVQQDEwlhLmZvby5jb20xGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2" +
+    "ZTETMBEGCgmSJomT8ixkARkWA29yZ4IJAK+nL4I3GkjeMAwGA1UdEwQFMAMBAf8w" +
+    "DQYJKoZIhvcNAQEFBQADggEBALam5DdoM7cyOS2GbiA7QAfZTJkBcVr4Fef9aDWR" +
+    "cG3kzbEbu1OXf3lkRW11H7gPLOgZGebSsxsv6YhKgAtz7py3lyH5QNkrN0OGI1ZA" +
+    "eXf76eSR4T26pYjxln26xyZUW/dcddQ0nSj9Yl52oFCWj38DqGaxP6hIu3DHGlcE" +
+    "PtpM2T4ZjWgrsqxL8N59zMb0Re9V4Xop7KmsLs3ThF3RWwmZdC1ba5LRPK6lKNF5" +
+    "CnSl5YzFUMnpzFZtneUhAHeFxrF+RV4f3bHLNs+sWjlmJo0ukCCnOzoiyE4oOJiL" +
+    "AhDym4nIfzng6fgYBeLT1Hp/bKHivQP4ef4wgre6r1ztnFA=";
+
+  /** Certificate with CN=*.foo.com. */
+  private static final String WC_FOO_COM_CERT =
+    "MIIDrzCCApegAwIBAgIJAJycqMrRasIKMA0GCSqGSIb3DQEBBQUAMEMxEjAQBgNV" +
+    "BAMUCSouZm9vLmNvbTEYMBYGCgmSJomT8ixkARkWCGxkYXB0aXZlMRMwEQYKCZIm" +
+    "iZPyLGQBGRYDb3JnMB4XDTEyMDExNzIyMjQ1N1oXDTIyMDExNDIyMjQ1N1owQzES" +
+    "MBAGA1UEAxQJKi5mb28uY29tMRgwFgYKCZImiZPyLGQBGRYIbGRhcHRpdmUxEzAR" +
+    "BgoJkiaJk/IsZAEZFgNvcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB" +
+    "AQDGRxBVvGZqHFWbYdbpOZaBf4H68b7zjiqbpXXq+mTfVOehIUeyL0624JsdmHLx" +
+    "oMNC7K9hAaM88wxcyhBRLRfo4ar1DJspzcFUoz2kFD7ytWGS2zwvV+VWnoXpPNiw" +
+    "9QuK6bdA/UYLIg/fk3TwshuoIT9VBJ4L3TRdOYYgH6WJBerQ2L5vMu91B9nBhNqR" +
+    "4RG8VFqwgwW9IoXBXC8XTZS5jd0bVoEoeA+PWVENQ3my5ilP4VUqo9h/jPdb8dFW" +
+    "3TNoaVHjOiTUOIpH+5cUmi0OkH2NzhTaWmCVoWuzFpvvB6PFHHxut2pDe8eGgc4x" +
+    "NdEvZDizbfY6JEb/fkwZ+Im9AgMBAAGjgaUwgaIwHQYDVR0OBBYEFPUscUXspD8Z" +
+    "LP3b6yybVhXhp5C2MHMGA1UdIwRsMGqAFPUscUXspD8ZLP3b6yybVhXhp5C2oUek" +
+    "RTBDMRIwEAYDVQQDFAkqLmZvby5jb20xGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2" +
+    "ZTETMBEGCgmSJomT8ixkARkWA29yZ4IJAJycqMrRasIKMAwGA1UdEwQFMAMBAf8w" +
+    "DQYJKoZIhvcNAQEFBQADggEBACG6nq5fSL8F1zHH0CP+sPWHJEh5OXErdhOfAKVc" +
+    "g0tfvYSI5gsyYTk87TZPTWkmpUUDn1keoVYqyXEaG8qAwL5cNUeYTze6R0GfB0UP" +
+    "jwmkCxZwKhZnN/ryXhzPIEJQHRsg2fYM0P2S6jUG9m92eyCUWrbolmwfkDotbvsS" +
+    "YE6m8oc7OaOVHQ20LDSLML3JOabONKSZW/BODI/ZzWzLNNU45xT4bGbtoyVwEerT" +
+    "WWsGAYdXbsREzuV9q3naEd4wl5CJRBFZtTIizM1RdxxbFrAhTkiDtURTERxLmFxY" +
+    "Nv3gLLhxykIoUIEtTxDjHgAiA02r3yBy5HfIC409WzmdVQI=";
+
+  /** Certificate with CN=*.foo.bar.com. */
+  private static final String WC_FOO_BAR_COM_CERT =
+    "MIIDuzCCAqOgAwIBAgIJAOxfZwQylIyjMA0GCSqGSIb3DQEBBQUAMEcxFjAUBgNV" +
+    "BAMUDSouZm9vLmJhci5jb20xGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2ZTETMBEG" +
+    "CgmSJomT8ixkARkWA29yZzAeFw0xMjAxMTgxNzE2MTBaFw0yMjAxMTUxNzE2MTBa" +
+    "MEcxFjAUBgNVBAMUDSouZm9vLmJhci5jb20xGDAWBgoJkiaJk/IsZAEZFghsZGFw" +
+    "dGl2ZTETMBEGCgmSJomT8ixkARkWA29yZzCCASIwDQYJKoZIhvcNAQEBBQADggEP" +
+    "ADCCAQoCggEBAMZHEFW8ZmocVZth1uk5loF/gfrxvvOOKpulder6ZN9U56EhR7Iv" +
+    "Trbgmx2YcvGgw0Lsr2EBozzzDFzKEFEtF+jhqvUMmynNwVSjPaQUPvK1YZLbPC9X" +
+    "5Vaehek82LD1C4rpt0D9RgsiD9+TdPCyG6ghP1UEngvdNF05hiAfpYkF6tDYvm8y" +
+    "73UH2cGE2pHhEbxUWrCDBb0ihcFcLxdNlLmN3RtWgSh4D49ZUQ1DebLmKU/hVSqj" +
+    "2H+M91vx0VbdM2hpUeM6JNQ4ikf7lxSaLQ6QfY3OFNpaYJWha7MWm+8Ho8UcfG63" +
+    "akN7x4aBzjE10S9kOLNt9jokRv9+TBn4ib0CAwEAAaOBqTCBpjAdBgNVHQ4EFgQU" +
+    "9SxxReykPxks/dvrLJtWFeGnkLYwdwYDVR0jBHAwboAU9SxxReykPxks/dvrLJtW" +
+    "FeGnkLahS6RJMEcxFjAUBgNVBAMUDSouZm9vLmJhci5jb20xGDAWBgoJkiaJk/Is" +
+    "ZAEZFghsZGFwdGl2ZTETMBEGCgmSJomT8ixkARkWA29yZ4IJAOxfZwQylIyjMAwG" +
+    "A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBABMssljEmqtJ1+2ci+l+8zzk" +
+    "Ak+xrkYNMWSjNVJ7B5pmD6MguMxfAiT2QNc0JaI0Zv4h+EprZeELQN3XsCwKRc13" +
+    "v+YuMyBH7xlXzvRQ+/0Y3x5BJKTUELzOdc95vhtwnPVfEwmNhzJAUXxfi0BnT9XZ" +
+    "J02ikAQ8RmtgeTUKDXLZP2xoIJ0YLc8dtdQ/M+ET6WH14kO01vqmk4ZX7oekHP2R" +
+    "W1oko9r9zXl9AKWqEd2p/hD8GiHdK2oS+Ob4Hc3k9UqxaAUxidsQmhRLBJKuHjIt" +
+    "GVqUK9J39FNxChacraSWTdx8yRQOxaKO5PfJDQRgCPg/9aV1AXQW+Y60ILvvHVA=";
+
+  /**
+   * Certificate with CN=a-c.foo.com
+   *                  subjAltName=DNS:a.foo.com,DNS:b.foo.com,DNS:c.foo.com.
+   */
+  private static final String A_FOO_COM_ALTNAME_CERT =
+    "MIIEAzCCAuugAwIBAgIJAMMwgpWWMq0YMA0GCSqGSIb3DQEBBQUAMEUxFDASBgNV" +
+    "BAMTC2EtYy5mb28uY29tMRgwFgYKCZImiZPyLGQBGRYIbGRhcHRpdmUxEzARBgoJ" +
+    "kiaJk/IsZAEZFgNvcmcwHhcNMTIwMTE4MTYxMDQwWhcNMjIwMTE1MTYxMDQwWjBF" +
+    "MRQwEgYDVQQDEwthLWMuZm9vLmNvbTEYMBYGCgmSJomT8ixkARkWCGxkYXB0aXZl" +
+    "MRMwEQYKCZImiZPyLGQBGRYDb3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB" +
+    "CgKCAQEAxkcQVbxmahxVm2HW6TmWgX+B+vG+844qm6V16vpk31TnoSFHsi9OtuCb" +
+    "HZhy8aDDQuyvYQGjPPMMXMoQUS0X6OGq9QybKc3BVKM9pBQ+8rVhkts8L1flVp6F" +
+    "6TzYsPULium3QP1GCyIP35N08LIbqCE/VQSeC900XTmGIB+liQXq0Ni+bzLvdQfZ" +
+    "wYTakeERvFRasIMFvSKFwVwvF02UuY3dG1aBKHgPj1lRDUN5suYpT+FVKqPYf4z3" +
+    "W/HRVt0zaGlR4zok1DiKR/uXFJotDpB9jc4U2lpglaFrsxab7wejxRx8brdqQ3vH" +
+    "hoHOMTXRL2Q4s232OiRG/35MGfiJvQIDAQABo4H1MIHyMAwGA1UdEwEB/wQCMAAw" +
+    "CwYDVR0PBAQDAgTwMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBT1LHFF" +
+    "7KQ/GSz92+ssm1YV4aeQtjB1BgNVHSMEbjBsgBT1LHFF7KQ/GSz92+ssm1YV4aeQ" +
+    "tqFJpEcwRTEUMBIGA1UEAxMLYS1jLmZvby5jb20xGDAWBgoJkiaJk/IsZAEZFghs" +
+    "ZGFwdGl2ZTETMBEGCgmSJomT8ixkARkWA29yZ4IJAMMwgpWWMq0YMCoGA1UdEQQj" +
+    "MCGCCWEuZm9vLmNvbYIJYi5mb28uY29tggljLmZvby5jb20wDQYJKoZIhvcNAQEF" +
+    "BQADggEBAH59Ewi4dxchcQwgJgA3KkTu6CAb/5S3BCwjv0ERdnnoshrxqu2lrF3e" +
+    "2oW16kGpPdQiIw0OdD/XB3o2It01PjDzdBBBgCas2JtpoQi7/QH0qrvgFqgbzPLV" +
+    "5Ehv1ObyxYKdOMDO7hqYr3PMkYyu4MhsjKp6LRDuFGHqYGzfdUzIjpfPd+jZtiN8" +
+    "EBH+ZmG/PueGFd+vaQu3CIGIkG9fLrfpckUD87x/n6pa+cuWvuAd814fWJpdvLl1" +
+    "iGkLfFU0E2G5pzlk9AHyWiBwYbuUrwLVW7sT7awpnzQBf0NCNETcuRmML7YnunwI" +
+    "3pJosuWr0LZy4fQbu3CquXgY9GNpto8=";
+
+  /**
+   * Certificate with CN=wc.foo.com
+   *                  subjAltName=DNS:*.foo.com.
+   */
+  private static final String WC_FOO_COM_ALTNAME_CERT =
+    "MIID6jCCAtKgAwIBAgIJAJrNbvmrBDUOMA0GCSqGSIb3DQEBBQUAMEQxEzARBgNV" +
+    "BAMTCndjLmZvby5jb20xGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2ZTETMBEGCgmS" +
+    "JomT8ixkARkWA29yZzAeFw0xMjAxMTgxNjI2MjJaFw0yMjAxMTUxNjI2MjJaMEQx" +
+    "EzARBgNVBAMTCndjLmZvby5jb20xGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2ZTET" +
+    "MBEGCgmSJomT8ixkARkWA29yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC" +
+    "ggEBAMZHEFW8ZmocVZth1uk5loF/gfrxvvOOKpulder6ZN9U56EhR7IvTrbgmx2Y" +
+    "cvGgw0Lsr2EBozzzDFzKEFEtF+jhqvUMmynNwVSjPaQUPvK1YZLbPC9X5Vaehek8" +
+    "2LD1C4rpt0D9RgsiD9+TdPCyG6ghP1UEngvdNF05hiAfpYkF6tDYvm8y73UH2cGE" +
+    "2pHhEbxUWrCDBb0ihcFcLxdNlLmN3RtWgSh4D49ZUQ1DebLmKU/hVSqj2H+M91vx" +
+    "0VbdM2hpUeM6JNQ4ikf7lxSaLQ6QfY3OFNpaYJWha7MWm+8Ho8UcfG63akN7x4aB" +
+    "zjE10S9kOLNt9jokRv9+TBn4ib0CAwEAAaOB3jCB2zAMBgNVHRMBAf8EAjAAMAsG" +
+    "A1UdDwQEAwIE8DATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQU9SxxReyk" +
+    "Pxks/dvrLJtWFeGnkLYwdAYDVR0jBG0wa4AU9SxxReykPxks/dvrLJtWFeGnkLah" +
+    "SKRGMEQxEzARBgNVBAMTCndjLmZvby5jb20xGDAWBgoJkiaJk/IsZAEZFghsZGFw" +
+    "dGl2ZTETMBEGCgmSJomT8ixkARkWA29yZ4IJAJrNbvmrBDUOMBQGA1UdEQQNMAuC" +
+    "CSouZm9vLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAcv8obBTxn7odtbjhc/Du36Zt" +
+    "T+HjeO4B8Claf1XgmX8lki2SDO2qOdwA0eaYcOJyKhbdIpspQrp7W8vzvSmN6NPg" +
+    "8XfAZ/xxDil8SfXwVjhHtAU4xYGeYRPY/1WCm8gKlWriV1ECRPn+sxs6DiG+HF7t" +
+    "fEwFBqg1m6FLGycm6H6NMSLL+1sr9MXqjSVetKIlzvGKi4ZdGMRjobGXSx12aCt9" +
+    "BfnIFAf8523sCADmpMs1th/blpzAfHkPXjtLa/6EC8Xj6EZfUaE8UGofgSpyS7wq" +
+    "2ICWGB2oi1ekDMQmP15GtyNm41B2s11KCdDhSCAJu0dyIqWztO3bAGVxR1YTtQ==";
+
+  /** Certificate with CN=127.0.0.1. */
+  private static final String LOCALHOST_CERT =
+    "MIIDrzCCApegAwIBAgIJAO+cKkPsfU8rMA0GCSqGSIb3DQEBBQUAMEMxEjAQBgNV" +
+    "BAMTCTEyNy4wLjAuMTEYMBYGCgmSJomT8ixkARkWCGxkYXB0aXZlMRMwEQYKCZIm" +
+    "iZPyLGQBGRYDb3JnMB4XDTEyMDExNzIyMzM0MFoXDTIyMDExNDIyMzM0MFowQzES" +
+    "MBAGA1UEAxMJMTI3LjAuMC4xMRgwFgYKCZImiZPyLGQBGRYIbGRhcHRpdmUxEzAR" +
+    "BgoJkiaJk/IsZAEZFgNvcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB" +
+    "AQDGRxBVvGZqHFWbYdbpOZaBf4H68b7zjiqbpXXq+mTfVOehIUeyL0624JsdmHLx" +
+    "oMNC7K9hAaM88wxcyhBRLRfo4ar1DJspzcFUoz2kFD7ytWGS2zwvV+VWnoXpPNiw" +
+    "9QuK6bdA/UYLIg/fk3TwshuoIT9VBJ4L3TRdOYYgH6WJBerQ2L5vMu91B9nBhNqR" +
+    "4RG8VFqwgwW9IoXBXC8XTZS5jd0bVoEoeA+PWVENQ3my5ilP4VUqo9h/jPdb8dFW" +
+    "3TNoaVHjOiTUOIpH+5cUmi0OkH2NzhTaWmCVoWuzFpvvB6PFHHxut2pDe8eGgc4x" +
+    "NdEvZDizbfY6JEb/fkwZ+Im9AgMBAAGjgaUwgaIwHQYDVR0OBBYEFPUscUXspD8Z" +
+    "LP3b6yybVhXhp5C2MHMGA1UdIwRsMGqAFPUscUXspD8ZLP3b6yybVhXhp5C2oUek" +
+    "RTBDMRIwEAYDVQQDEwkxMjcuMC4wLjExGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2" +
+    "ZTETMBEGCgmSJomT8ixkARkWA29yZ4IJAO+cKkPsfU8rMAwGA1UdEwQFMAMBAf8w" +
+    "DQYJKoZIhvcNAQEFBQADggEBAEy+LQguZ0kDdRone/HDnNQfCtWplHU8rE/8oFZo" +
+    "ZVroGGo55zu5Iv66AljeLkTBp7FqIhH9JbwB8CF57g0Uuok560ttoWV/RPisW86p" +
+    "z7eURpPClyel5+uz/PUt8crdNhXqG5iRvO7NlJONVZfLf3KlilXcoSE13msv8X80" +
+    "pDXqOv61kZ4CKB1eAWMT5PXLsks47g42OtHKdOrGv+KGyiMUXmO/9Jxa44maXP6x" +
+    "s8nJ1c5f2zZaZEANTkvO6UFbYynAHisBn9xD++5OcjVJMgX1qOaoxurO2kov5oyw" +
+    "bLLuQaV6NVa+DPs6X6P1+iAmPQNj+Izqveq+8C1vyYdu9VU=";
+
+  /**
+   * Certificate with CN=localhost
+   *                  subjAltName=IP:127.0.0.1.
+   */
+  private static final String LOCALHOST_ALTNAME_CERT =
+    "MIID4jCCAsqgAwIBAgIJAK/f77u+7Kw2MA0GCSqGSIb3DQEBBQUAMEMxEjAQBgNV" +
+    "BAMTCWxvY2FsaG9zdDEYMBYGCgmSJomT8ixkARkWCGxkYXB0aXZlMRMwEQYKCZIm" +
+    "iZPyLGQBGRYDb3JnMB4XDTEyMDExODE2MDY1NFoXDTIyMDExNTE2MDY1NFowQzES" +
+    "MBAGA1UEAxMJbG9jYWxob3N0MRgwFgYKCZImiZPyLGQBGRYIbGRhcHRpdmUxEzAR" +
+    "BgoJkiaJk/IsZAEZFgNvcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB" +
+    "AQDGRxBVvGZqHFWbYdbpOZaBf4H68b7zjiqbpXXq+mTfVOehIUeyL0624JsdmHLx" +
+    "oMNC7K9hAaM88wxcyhBRLRfo4ar1DJspzcFUoz2kFD7ytWGS2zwvV+VWnoXpPNiw" +
+    "9QuK6bdA/UYLIg/fk3TwshuoIT9VBJ4L3TRdOYYgH6WJBerQ2L5vMu91B9nBhNqR" +
+    "4RG8VFqwgwW9IoXBXC8XTZS5jd0bVoEoeA+PWVENQ3my5ilP4VUqo9h/jPdb8dFW" +
+    "3TNoaVHjOiTUOIpH+5cUmi0OkH2NzhTaWmCVoWuzFpvvB6PFHHxut2pDe8eGgc4x" +
+    "NdEvZDizbfY6JEb/fkwZ+Im9AgMBAAGjgdgwgdUwDAYDVR0TAQH/BAIwADALBgNV" +
+    "HQ8EBAMCBPAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFPUscUXspD8Z" +
+    "LP3b6yybVhXhp5C2MHMGA1UdIwRsMGqAFPUscUXspD8ZLP3b6yybVhXhp5C2oUek" +
+    "RTBDMRIwEAYDVQQDEwlsb2NhbGhvc3QxGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2" +
+    "ZTETMBEGCgmSJomT8ixkARkWA29yZ4IJAK/f77u+7Kw2MA8GA1UdEQQIMAaHBH8A" +
+    "AAEwDQYJKoZIhvcNAQEFBQADggEBAGa/YCXT/zUV48INqggR0ielSIXz1ztFKG4R" +
+    "sWDoh76MPwyDqONXA3azXKe5BkeXDQ+6cN+VTgHYCpaHnaWdAWgLVqs4prr5MIzk" +
+    "pxIRiVbayzRi0apUq5MyV/XGMECYOf3dPCT2P9Ph4jGJkLHKg66cKoxEPreoCToy" +
+    "GT/1gh18bJ0xAo1CMlc4rH5C1pOx+hOIurFIxjUg44TGnBxMYUmeH0S1B1rmkuFo" +
+    "h65ugoRzPU690x6DkscPxSQKexEjEZG+z0QnsQgaig6SY3bX2kKMa48QywLp0/Vo" +
+    "HddtVv0q6rQqonRHRuCyD+FuXUg0w7BVVRH9txYAsE5eciIc7z0=";
+
+
+  /**
+   * Certificate test data.
+   *
+   * @return  cert test data
+   *
+   * @throws  Exception  if test data cannot be generated
+   */
+  @DataProvider(name = "certificates")
+  public Object[][] createCerts()
+    throws Exception
+  {
+    final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+    final X509Certificate aFooComCert =
+      (X509Certificate) cf.generateCertificate(
+        new ByteArrayInputStream(LdapUtil.base64Decode(A_FOO_COM_CERT)));
+    final X509Certificate wcFooComCert =
+      (X509Certificate) cf.generateCertificate(
+        new ByteArrayInputStream(LdapUtil.base64Decode(WC_FOO_COM_CERT)));
+    final X509Certificate wcFooBarComCert =
+      (X509Certificate) cf.generateCertificate(
+        new ByteArrayInputStream(LdapUtil.base64Decode(WC_FOO_BAR_COM_CERT)));
+    final X509Certificate aFooComAltNameCert =
+      (X509Certificate) cf.generateCertificate(
+        new ByteArrayInputStream(
+          LdapUtil.base64Decode(A_FOO_COM_ALTNAME_CERT)));
+    final X509Certificate wcFooComAltNameCert =
+      (X509Certificate) cf.generateCertificate(
+        new ByteArrayInputStream(
+          LdapUtil.base64Decode(WC_FOO_COM_ALTNAME_CERT)));
+    final X509Certificate localhostCert =
+      (X509Certificate) cf.generateCertificate(
+        new ByteArrayInputStream(LdapUtil.base64Decode(LOCALHOST_CERT)));
+    final X509Certificate localhostAltNameCert =
+      (X509Certificate) cf.generateCertificate(
+        new ByteArrayInputStream(
+          LdapUtil.base64Decode(LOCALHOST_ALTNAME_CERT)));
+
+    return new Object[][] {
+      /* a.foo.com == CN=a.foo.com */
+      new Object[] {
+        "a.foo.com", aFooComCert, true,
+      },
+      /* b.foo.com != CN=a.foo.com */
+      new Object[] {
+        "b.foo.com", aFooComCert, false,
+      },
+      /* a.foo.com == CN=*.foo.com */
+      new Object[] {
+        "a.foo.com", wcFooComCert, true,
+      },
+      /* b.foo.com == CN=*.foo.com */
+      new Object[] {
+        "b.foo.com", wcFooComCert, true,
+      },
+      /* a.b.foo.com != CN=*.foo.com */
+      new Object[] {
+        "a.b.foo.com", wcFooComCert, false,
+      },
+      /* a.foo.com != CN=*.foo.bar.com */
+      new Object[] {
+        "a.foo.com", wcFooBarComCert, false,
+      },
+      /* a.b.foo.bar.com != CN=*.foo.bar.com */
+      new Object[] {
+        "a.b.foo.bar.com", wcFooBarComCert, false,
+      },
+      /* a.foo.bar.com == CN=*.foo.bar.com */
+      new Object[] {
+        "a.foo.bar.com", wcFooBarComCert, true,
+      },
+      /* a.foo.com == subjAltName: DNS=a.foo.com */
+      new Object[] {
+        "a.foo.com", aFooComAltNameCert, true,
+      },
+      /* b.foo.com == subjAltName: DNS=b.foo.com */
+      new Object[] {
+        "b.foo.com", aFooComAltNameCert, true,
+      },
+      /* a.foo.com == subjAltName: DNS=*.foo.com */
+      new Object[] {
+        "a.foo.com", wcFooComAltNameCert, true,
+      },
+      /* b.foo.com == subjAltName: DNS=*.foo.com */
+      new Object[] {
+        "b.foo.com", wcFooComAltNameCert, true,
+      },
+      /* a.b.foo.com != subjAltName: DNS=*.foo.com */
+      new Object[] {
+        "a.b.foo.com", wcFooComAltNameCert, false,
+      },
+      /* 10.0.0.1 != CN=127.0.0.1 */
+      new Object[] {
+        "10.0.0.1", localhostCert, false,
+      },
+      /* 127.0.0.1 != CN=127.0.0.1, IPs can only match subjAltName */
+      new Object[] {
+        "127.0.0.1", localhostCert, false,
+      },
+      /* 127.0.0.1 == subjAltName: IP=127.0.0.1 */
+      new Object[] {
+        "127.0.0.1", localhostAltNameCert, true,
+      },
+    };
+  }
+
+
+  /**
+   * @param  hostname  to match against the cert
+   * @param  cert  to extract hostname from
+   * @param  pass  whether the verify should succeed
+   *
+   * @throws  Exception  On test failure.
+   */
+  @Test(groups = {"ssl"}, dataProvider = "certificates")
+  public void verify(
+    final String hostname, final X509Certificate cert, final boolean pass)
+    throws Exception
+  {
+    final boolean defaultResult = DEFAULT_VERIFIER.verify(hostname, cert);
+    final boolean sunResult = SUN_VERIFIER.verify(hostname, cert);
+    Assert.assertEquals(defaultResult, sunResult);
+    Assert.assertEquals(defaultResult, pass);
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/ssl/SunTLSHostnameVerifier.java b/src/test/java/edu/vt/middleware/ldap/ssl/SunTLSHostnameVerifier.java
new file mode 100644
index 0000000..5835b07
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/ssl/SunTLSHostnameVerifier.java
@@ -0,0 +1,69 @@
+/*
+  $Id: SunTLSHostnameVerifier.java 2217 2012-01-23 19:56:35Z dfisher $
+
+  Copyright (C) 2003-2012 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 2217 $
+  Updated: $Date: 2012-01-23 19:56:35 +0000 (Mon, 23 Jan 2012) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import sun.security.util.HostnameChecker;
+
+/**
+ * A {@link HostnameVerifier} that delegates to the internal Sun implementation
+ * at sun.security.util.HostnameChecker. This is the implementation used by
+ * JNDI with StartTLS.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 2217 $ $Date: 2012-01-23 19:56:35 +0000 (Mon, 23 Jan 2012) $
+ */
+public class SunTLSHostnameVerifier implements HostnameVerifier
+{
+
+
+  /** {@inheritDoc} */
+  public boolean verify(final String hostname, final SSLSession session)
+  {
+    boolean b = false;
+    try {
+      b = verify(hostname, (X509Certificate) session.getPeerCertificates()[0]);
+    } catch (SSLPeerUnverifiedException e) {
+      b = false;
+    }
+    return b;
+  }
+
+
+  /**
+   * Expose convenience method for testing.
+   *
+   * @param  hostname  to verify
+   * @param  cert  to verify hostname against
+   *
+   * @return  whether the certificate is allowed
+   */
+  public boolean verify(final String hostname, final X509Certificate cert)
+  {
+    boolean b = false;
+    final HostnameChecker checker = HostnameChecker.getInstance(
+      HostnameChecker.TYPE_LDAP);
+    try {
+      checker.match(hostname, cert);
+      b = true;
+    } catch (CertificateException e) {
+      b = false;
+    }
+    return b;
+  }
+}
diff --git a/src/test/java/edu/vt/middleware/ldap/ssl/TLSSocketFactoryTest.java b/src/test/java/edu/vt/middleware/ldap/ssl/TLSSocketFactoryTest.java
new file mode 100644
index 0000000..46a5ee9
--- /dev/null
+++ b/src/test/java/edu/vt/middleware/ldap/ssl/TLSSocketFactoryTest.java
@@ -0,0 +1,208 @@
+/*
+  $Id: TLSSocketFactoryTest.java 1486 2010-08-17 18:53:58Z dfisher $
+
+  Copyright (C) 2003-2010 Virginia Tech.
+  All rights reserved.
+
+  SEE LICENSE FOR MORE INFORMATION
+
+  Author:  Middleware Services
+  Email:   middleware at vt.edu
+  Version: $Revision: 1486 $
+  Updated: $Date: 2010-08-17 19:53:58 +0100 (Tue, 17 Aug 2010) $
+*/
+package edu.vt.middleware.ldap.ssl;
+
+import java.util.Arrays;
+import javax.net.ssl.SSLSocket;
+import edu.vt.middleware.ldap.AnyHostnameVerifier;
+import edu.vt.middleware.ldap.Ldap;
+import edu.vt.middleware.ldap.TestUtil;
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test for {@link TLSSocketFactory}.
+ *
+ * @author  Middleware Services
+ * @version  $Revision: 1486 $
+ */
+public class TLSSocketFactoryTest
+{
+
+  /** List of ciphers. */
+  public static final String[] CIPHERS = new String[] {
+    "TLS_DH_anon_WITH_AES_128_CBC_SHA",
+    "TLS_DH_anon_WITH_AES_256_CBC_SHA",
+    "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA",
+    "SSL_DH_anon_WITH_RC4_128_MD5",
+    "TLS_RSA_WITH_AES_128_CBC_SHA",
+    "TLS_RSA_WITH_AES_256_CBC_SHA",
+    "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
+    "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
+    "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
+    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
+    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
+    "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
+    "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
+    "SSL_RSA_WITH_RC4_128_MD5",
+    "SSL_RSA_WITH_RC4_128_SHA",
+  };
+
+  /** List of ciphers. */
+  public static final String[] UNKNOWN_CIPHERS = new String[] {
+    "TLS_DH_anon_WITH_AES_128_CBC_SHA",
+    "TLS_DH_anon_WITH_3DES_256_CBC_SHA",
+    "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA",
+    "SSL_DH_anon_WITH_RC4_128_MD5",
+  };
+
+  /** List of protocols. */
+  public static final String[] ALL_PROTOCOLS = new String[] {
+    "SSLv2Hello",
+    "SSLv3",
+    "TLSv1",
+  };
+
+  /** List of protocols. */
+  public static final String[] PROTOCOLS = new String[] {
+    "SSLv3",
+    "TLSv1",
+  };
+
+  /** List of protocols. */
+  public static final String[] FAIL_PROTOCOLS = new String[] {
+    "SSLv2Hello",
+  };
+
+  /** List of protocols. */
+  public static final String[] UNKNOWN_PROTOCOLS = new String[] {
+    "SSLv2Hello",
+    "SSLv3Hello",
+    "TLSv1",
+  };
+
+
+  /**
+   * @return  <code>Ldap</code>
+   *
+   * @throws  Exception  On ldap construction failure.
+   */
+  public Ldap createTLSLdap()
+    throws Exception
+  {
+    // configure TLSSocketFactory
+    final X509CertificatesCredentialReader reader =
+      new X509CertificatesCredentialReader();
+    final X509SSLContextInitializer ctxInit =
+      new X509SSLContextInitializer();
+    ctxInit.setTrustCertificates(
+      reader.read("file:src/test/resources/ed.trust.crt"));
+    final TLSSocketFactory sf = new TLSSocketFactory();
+    sf.setSSLContextInitializer(ctxInit);
+    sf.initialize();
+
+    // configure ldap object to use TLS
+    final Ldap ldap = TestUtil.createLdap();
+    ldap.getLdapConfig().setTls(true);
+    ldap.getLdapConfig().setSslSocketFactory(sf);
+    ldap.getLdapConfig().setHostnameVerifier(new AnyHostnameVerifier());
+    return ldap;
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"ssltest"})
+  public void setEnabledCipherSuites()
+    throws Exception
+  {
+    final Ldap ldap = this.createTLSLdap();
+    final TLSSocketFactory sf =
+      (TLSSocketFactory) ldap.getLdapConfig().getSslSocketFactory();
+
+    AssertJUnit.assertTrue(ldap.connect());
+    ldap.getSchema("ou=test,dc=vt,dc=edu");
+    AssertJUnit.assertEquals(
+      Arrays.asList(((SSLSocket) sf.createSocket()).getEnabledCipherSuites()),
+      Arrays.asList(sf.getDefaultCipherSuites()));
+    AssertJUnit.assertNotSame(
+      Arrays.asList(sf.getDefaultCipherSuites()), Arrays.asList(CIPHERS));
+    ldap.close();
+
+    sf.setEnabledCipherSuites(UNKNOWN_CIPHERS);
+    try {
+      ldap.connect();
+      AssertJUnit.fail(
+        "Should have thrown IllegalArgumentException, no exception thrown");
+    } catch (IllegalArgumentException e) {
+      AssertJUnit.assertEquals(IllegalArgumentException.class, e.getClass());
+    } catch (Exception e) {
+      AssertJUnit.fail(
+        "Should have thrown IllegalArgumentException, threw " + e);
+    }
+    ldap.close();
+
+    sf.setEnabledCipherSuites(CIPHERS);
+    AssertJUnit.assertTrue(ldap.connect());
+    ldap.getSchema("ou=test,dc=vt,dc=edu");
+    AssertJUnit.assertEquals(
+      Arrays.asList(((SSLSocket) sf.createSocket()).getEnabledCipherSuites()),
+      Arrays.asList(CIPHERS));
+    ldap.close();
+  }
+
+
+  /** @throws  Exception  On test failure. */
+  @Test(groups = {"ssltest"})
+  public void setEnabledProtocols()
+    throws Exception
+  {
+    final Ldap ldap = this.createTLSLdap();
+    final TLSSocketFactory sf =
+      (TLSSocketFactory) ldap.getLdapConfig().getSslSocketFactory();
+
+    AssertJUnit.assertTrue(ldap.connect());
+    ldap.getSchema("ou=test,dc=vt,dc=edu");
+    AssertJUnit.assertEquals(
+      Arrays.asList(((SSLSocket) sf.createSocket()).getEnabledProtocols()),
+      Arrays.asList(ALL_PROTOCOLS));
+    AssertJUnit.assertNotSame(
+      Arrays.asList(((SSLSocket) sf.createSocket()).getEnabledProtocols()),
+      Arrays.asList(PROTOCOLS));
+    ldap.close();
+
+    sf.setEnabledProtocols(FAIL_PROTOCOLS);
+    try {
+      ldap.connect();
+      AssertJUnit.fail(
+        "Should have thrown IllegalArgumentException, no exception thrown");
+    } catch (IllegalArgumentException e) {
+      AssertJUnit.assertEquals(IllegalArgumentException.class, e.getClass());
+    } catch (Exception e) {
+      AssertJUnit.fail(
+        "Should have thrown IllegalArgumentException, threw " + e);
+    }
+    ldap.close();
+
+    sf.setEnabledProtocols(UNKNOWN_PROTOCOLS);
+    try {
+      ldap.connect();
+      AssertJUnit.fail(
+        "Should have thrown IllegalArgumentException, no exception thrown");
+    } catch (IllegalArgumentException e) {
+      AssertJUnit.assertEquals(IllegalArgumentException.class, e.getClass());
+    } catch (Exception e) {
+      AssertJUnit.fail(
+        "Should have thrown IllegalArgumentException, threw " + e);
+    }
+    ldap.close();
+
+    sf.setEnabledProtocols(PROTOCOLS);
+    AssertJUnit.assertTrue(ldap.connect());
+    ldap.getSchema("ou=test,dc=vt,dc=edu");
+    AssertJUnit.assertEquals(
+      Arrays.asList(((SSLSocket) sf.createSocket()).getEnabledProtocols()),
+      Arrays.asList(PROTOCOLS));
+    ldap.close();
+  }
+}
diff --git a/src/test/resources/ed.keystore b/src/test/resources/ed.keystore
new file mode 100644
index 0000000..72750f0
Binary files /dev/null and b/src/test/resources/ed.keystore differ
diff --git a/src/test/resources/ed.trust.crt b/src/test/resources/ed.trust.crt
new file mode 100644
index 0000000..5c61ef7
--- /dev/null
+++ b/src/test/resources/ed.trust.crt
@@ -0,0 +1,42 @@
+-----BEGIN CERTIFICATE-----
+MIIDhzCCAvCgAwIBAgIJAPpeFAkJP5xgMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD
+VQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExEzARBgNVBAcTCkJsYWNrc2J1cmcx
+FjAUBgNVBAoTDVZpcmdpbmlhIFRlY2gxEzARBgNVBAsTCk1pZGRsZXdhcmUxJjAk
+BgNVBAMTHWxkYXAtdGVzdC0xLm1pZGRsZXdhcmUudnQuZWR1MB4XDTExMDkyNjE2
+NDczOFoXDTIxMDkyMzE2NDczOFowgYoxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhW
+aXJnaW5pYTETMBEGA1UEBxMKQmxhY2tzYnVyZzEWMBQGA1UEChMNVmlyZ2luaWEg
+VGVjaDETMBEGA1UECxMKTWlkZGxld2FyZTEmMCQGA1UEAxMdbGRhcC10ZXN0LTEu
+bWlkZGxld2FyZS52dC5lZHUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJWf
+/vBsfFn6sQo57IHrBzMlPARpDI1DJeqH7zl2UeVzeiZDjGiU4ETSjEsvvQRzLfXZ
+IgJEr1IEAzjCX8wKF4svrmkPK3KN6JvdlknM7Thw5p0NzAh2Bq1R1h7+bUvQJGep
+aizNM0od/mKrJnOnUCWEgcpG91mWg8b1PphGobeNAgMBAAGjgfIwge8wHQYDVR0O
+BBYEFMT2Hkcp6JFq242hWfdMOeT3/hZ1MIG/BgNVHSMEgbcwgbSAFMT2Hkcp6JFq
+242hWfdMOeT3/hZ1oYGQpIGNMIGKMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmly
+Z2luaWExEzARBgNVBAcTCkJsYWNrc2J1cmcxFjAUBgNVBAoTDVZpcmdpbmlhIFRl
+Y2gxEzARBgNVBAsTCk1pZGRsZXdhcmUxJjAkBgNVBAMTHWxkYXAtdGVzdC0xLm1p
+ZGRsZXdhcmUudnQuZWR1ggkA+l4UCQk/nGAwDAYDVR0TBAUwAwEB/zANBgkqhkiG
+9w0BAQUFAAOBgQBe0bV5iZyPupNh2zmdH7opuwldz1sxlkRdUQhKSlYsOqgAKDvS
+DypmR4mqntAULTFGZIdcQ1W8HJcnRc8KuPfNatAV8A9OqMbtDLnmfWkl33JPiDUd
+fIKCXuG4dZ6nn3RbjlKhXzHYADmJzdQNIC3M9eDQBEYmMy8+mV+ErVebBg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDYjCCAkqgAwIBAgIETwyocDANBgkqhkiG9w0BAQUFADBzMRMwEQYKCZImiZPy
+LGQBGRYDZWR1MRIwEAYKCZImiZPyLGQBGRYCdnQxCzAJBgNVBAoTAnZ0MRMwEQYD
+VQQLEwptaWRkbGV3YXJlMSYwJAYDVQQDEx1sZGFwLXRlc3QtMi5taWRkbGV3YXJl
+LnZ0LmVkdTAeFw0xMjAxMTAyMTA2NTZaFw0xNDAxMDkyMTA2NTZaMHMxEzARBgoJ
+kiaJk/IsZAEZFgNlZHUxEjAQBgoJkiaJk/IsZAEZFgJ2dDELMAkGA1UEChMCdnQx
+EzARBgNVBAsTCm1pZGRsZXdhcmUxJjAkBgNVBAMTHWxkYXAtdGVzdC0yLm1pZGRs
+ZXdhcmUudnQuZWR1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm5xR
+Lm8+NlxUJSR/2tXZrYW8+9kQSJAR+a/xfcxcWETntbLHE4XZzCde28wDRM5QZo5z
+fK91upR3uf4/O+4xmOgiBarP6x1Dtob6JeMeUh6/lSpNuNdkSEbSZ3gS0Vc/OYIr
+mcwLQroqNxi3BPVy6ryzGSnd9/PhOf5R2C3mZcBunCQU9GyEexlvQ4d8UkYBNKz5
+sEHl4hcIIirDNTJ27v752rjVWkxPOtF6KUeRsarP4qK2N5nOipAGRrG5nON5ggFl
+YC3UNdj3tvoDstxRSgPY9RaHgN5uNrMPmLrbwNY3q9YsiRZ7veiqIIvirGc3CXfc
+4tTD5sClKhafguf6BQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQCMvZzuxAVc/G7W
+4Ci/Oxa6YAH6Yq745SVuVAjBAhD9wz+Ney4cuFHeyPFnbYWNlikgeWpoglLkEBta
+SGxSnVM74ZxGLwpaWOuI5d2tv7bWm318a0kDaTiXRrJ+6tg/rjyQ7xxYlI5+CqO+
+nK+rB4hawRhRvzguItNzKJhnCuydM5lZoXMZAPDxRxDy/2LfokvaCKpNjCYb+nGE
+Q66oXAH7AP4OynJ2B5fu4eNgbr8JH4dRH667Y+Pkb03xWabdvp5b+YLmP2iOuq10
+DDm9bpasI5c/SfWdZfWTC3a12WdzZaGBLVxlTRJvegAaktEBXm79OtoSr4TJtNLC
+QTHsT7nJ
+-----END CERTIFICATE-----
diff --git a/src/test/resources/ed.truststore b/src/test/resources/ed.truststore
new file mode 100644
index 0000000..4940c6c
Binary files /dev/null and b/src/test/resources/ed.truststore differ
diff --git a/src/test/resources/edu/vt/middleware/ldap/binaryResults.ldif b/src/test/resources/edu/vt/middleware/ldap/binaryResults.ldif
new file mode 100644
index 0000000..f4436cf
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/binaryResults.ldif
@@ -0,0 +1,5 @@
+dn: uid=2,ou=test,dc=vt,dc=edu
+mail: jdoe2 at vt.edu
+uid: 2
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-2.ldif b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-2.ldif
new file mode 100644
index 0000000..41f9c06
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-2.ldif
@@ -0,0 +1,6 @@
+dn: uugid=group2,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2002
+uugid: group2
diff --git a/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-3.ldif b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-3.ldif
new file mode 100644
index 0000000..af80618
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-3.ldif
@@ -0,0 +1,6 @@
+dn: uugid=group3,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2003
+uugid: group3
diff --git a/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-4.ldif b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-4.ldif
new file mode 100644
index 0000000..1c182cf
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-4.ldif
@@ -0,0 +1,6 @@
+dn: uugid=group4,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2004
+uugid: group4
diff --git a/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-5.ldif b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-5.ldif
new file mode 100644
index 0000000..1396387
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-5.ldif
@@ -0,0 +1,6 @@
+dn: uugid=group5,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2005
+uugid: group5
diff --git a/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-6.ldif b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-6.ldif
new file mode 100644
index 0000000..4280be0
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-6.ldif
@@ -0,0 +1,6 @@
+dn: uugid=group6,ou=test,dc=vt,dc=edu
+contactPerson: uid=7,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2006
+uugid: group6
diff --git a/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-7.ldif b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-7.ldif
new file mode 100644
index 0000000..9c73ac1
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-7.ldif
@@ -0,0 +1,6 @@
+dn: uugid=group7,ou=test,dc=vt,dc=edu
+contactPerson: uid=7,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2007
+uugid: group7
diff --git a/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-8.ldif b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-8.ldif
new file mode 100644
index 0000000..8c58939
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-8.ldif
@@ -0,0 +1,6 @@
+dn: uugid=group8,ou=test,dc=vt,dc=edu
+contactPerson: uid=7,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2008
+uugid: group8
diff --git a/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-9.ldif b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-9.ldif
new file mode 100644
index 0000000..60331c5
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createGroupEntry-9.ldif
@@ -0,0 +1,6 @@
+dn: uugid=group9,ou=test,dc=vt,dc=edu
+contactPerson: uid=7,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2009
+uugid: group9
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-10.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-10.ldif
new file mode 100644
index 0000000..f3041c3
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-10.ldif
@@ -0,0 +1,16 @@
+dn: uid=10,ou=test,dc=vt,dc=edu
+cn: Johv H Dom
+departmentNumber: 0830
+displayName: Johv H Dom
+givenName: Johv
+initials: JHD
+mail: jdoe10 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Dom
+uid: 10
+userPassword: {SHA}9o7EHN4W9rgG17BMcFdmtzGPux0=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-11.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-11.ldif
new file mode 100644
index 0000000..dbe6318
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-11.ldif
@@ -0,0 +1,15 @@
+dn: uid=11,ou=test,dc=vt,dc=edu
+cn: Johw H Don
+departmentNumber: 0830
+displayName: Johw H Don
+givenName: Johw
+initials: JHD
+mail: jdoe11 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Don
+uid: 11
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-12.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-12.ldif
new file mode 100644
index 0000000..e137f35
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-12.ldif
@@ -0,0 +1,15 @@
+dn: uid=12,ou=test,dc=vt,dc=edu
+cn: Johx H Doo
+departmentNumber: 0830
+displayName: Johx H Doo
+givenName: Johx
+initials: JHD
+mail: jdoe12 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Doo
+uid: 12
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-2.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-2.ldif
new file mode 100644
index 0000000..932a32f
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-2.ldif
@@ -0,0 +1,16 @@
+dn: uid=2,ou=test,dc=vt,dc=edu
+cn: John H Doe
+departmentNumber: 0822
+displayName: John H Doe
+givenName: John
+initials: JHD
+mail: jdoe2 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Doe
+uid: 2
+userPassword: {SHA}KqYKj/f81HPTIeAUav2eJt85UUc=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-3.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-3.ldif
new file mode 100644
index 0000000..0f00d3c
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-3.ldif
@@ -0,0 +1,16 @@
+dn: uid=3,ou=test,dc=vt,dc=edu
+cn: Joho H Dof
+departmentNumber: 0823
+displayName: Joho H Dof
+givenName: Joho
+initials: JHD
+mail: jdoe3 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Dof
+uid: 3
+userPassword: {SHA}ERnP037iRzV+A0oI2ETuol9v0g8=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-4.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-4.ldif
new file mode 100644
index 0000000..2a8eead
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-4.ldif
@@ -0,0 +1,16 @@
+dn: uid=4,ou=test,dc=vt,dc=edu
+cn: Johp H Dog
+departmentNumber: 0824
+displayName: Johp H Dog
+givenName: Johp
+initials: JHD
+mail: jdoe4 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Dog
+uid: 4
+userPassword: {SHA}oddYTarKRzjUma1wgohrARFyddg=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-5.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-5.ldif
new file mode 100644
index 0000000..fab4e53
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-5.ldif
@@ -0,0 +1,16 @@
+dn: uid=5,ou=test,dc=vt,dc=edu
+cn: Johq H Doh
+departmentNumber: 0825
+displayName: Johq H Doh
+givenName: Johq
+initials: JHD
+mail: jdoe5 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Doh
+uid: 5
+userPassword: {SHA}7bqVXQ6hX9709hcm75flr1B0MMA=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-6.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-6.ldif
new file mode 100644
index 0000000..4bc39b5
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-6.ldif
@@ -0,0 +1,16 @@
+dn: uid=6,ou=test,dc=vt,dc=edu
+cn: Johr H Doi
+departmentNumber: 0826
+displayName: Johr H Doi
+givenName: Johr
+initials: JHD
+mail: jdoe6 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Doi
+uid: 6
+userPassword: {SHA}bXSeijeKNM8ZtMAveVX1f9uhMKU=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-7.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-7.ldif
new file mode 100644
index 0000000..66a8e85
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-7.ldif
@@ -0,0 +1,24 @@
+dn: uid=7,ou=test,dc=vt,dc=edu
+cn: Johs H Doj
+creationDate: 2015-06-06T12:00:00
+departmentNumber: 0827
+displayName: Johs H Doj
+eduPersonAffiliation: student
+eduPersonAffiliation: staff
+eduPersonPrimaryAffiliation: student
+givenName: Johs
+initials: JHD
+mail: jdoe7 at vt.edu
+groupMembership: uugid=group6,ou=test,dc=vt,dc=edu
+groupMembership: uugid=group9,ou=test,dc=vt,dc=edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: virginiaTechPerson
+objectClass: top
+personType: Virginia Tech Person
+sn: Doj
+uid: 7
+userPassword: {SHA}MwumDiQxhun6JY+Zkth2bqboi8E=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-8.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-8.ldif
new file mode 100644
index 0000000..af33503
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-8.ldif
@@ -0,0 +1,16 @@
+dn: uid=8,ou=test,dc=vt,dc=edu
+cn: Joht H Dok
+departmentNumber: 0828
+displayName: Joht H Dok
+givenName: Joht
+initials: JHD
+mail: jdoe8 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Dok
+uid: 8
+userPassword: {SHA}qNu/pBzsgz+N1Cvk0fqaExQshcI=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-9.ldif b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-9.ldif
new file mode 100644
index 0000000..b58ba56
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/createLdapEntry-9.ldif
@@ -0,0 +1,16 @@
+dn: uid=9,ou=test,dc=vt,dc=edu
+cn: Johu H Dol
+departmentNumber: 0829
+displayName: Johu H Dol
+givenName: Johu
+initials: JHD
+mail: jdoe9 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Dol
+uid: 9
+userPassword: {SHA}AksBkW4+rsZqLEtvxYexcF8ab8g=
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/dfisher.dsmlv1 b/src/test/resources/edu/vt/middleware/ldap/dfisher.dsmlv1
new file mode 100644
index 0000000..489568e
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/dfisher.dsmlv1
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<dsml:dsml xmlns:dsml="http://www.dsml.org/DSML">
+  <dsml:directory-entries>
+    <dsml:entry dn="uid=818037,ou=People,dc=vt,dc=edu">
+      <dsml:objectclass>
+        <dsml:oc-value>virginiaTechPerson</dsml:oc-value>
+      </dsml:objectclass>
+      <dsml:attr name="title">
+        <dsml:value>Project Manager, Middleware</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="uid">
+        <dsml:value>818037</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="authId">
+        <dsml:value>dfisher</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="mail">
+        <dsml:value>dfisher at vt.edu</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="instantMessagingID">
+        <dsml:value>Yahoo!:dfish3r</dsml:value>
+        <dsml:value>Google:dfisher at gmail.com</dsml:value>
+        <dsml:value>Virginia Tech:dfisher at im.vt.edu</dsml:value>
+        <dsml:value>ICQ:8168282</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="department">
+        <dsml:value>MiddleWare</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="sn">
+        <dsml:value>Fisher</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="userCertificate;binary">
+        <dsml:value encoding="base64">MIIC7zCCAligAwIBAgIQCBdZgzdx+yX5+jSRD50SJzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwNjI5MTg0NTQ2WhcNMTAwNjI5MTg0NTQ2WjBZMQ8wDQYDVQQEEwZGaXNoZXIxDzANBgNVBCoTBkRhbmllbDEWMBQGA1UEAxMNRGFuaWVsIEZpc2hlcjEdMBsGCSqGSIb3DQEJARYOZGZpc2hlckB2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbsC0qvAkU3oWwz39oq/ur/mhHZ+KNODug+pAsg0ZZbSnxWD9EVB [...]
+      </dsml:attr>
+      <dsml:attr name="cn">
+        <dsml:value>Daniel W Fisher</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="mailStop">
+        <dsml:value>0479</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="suppressDisplay">
+        <dsml:value>false</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="suppressedAttribute">
+        <dsml:value>localPostalAddress</dsml:value>
+        <dsml:value>homePostalAddress</dsml:value>
+        <dsml:value>homePhone</dsml:value>
+        <dsml:value>localPhone</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="postalAddress">
+        <dsml:value>SETI-Middleware$1700 Pratt Drive$Blacksburg, VA 24060</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="departmentNumber">
+        <dsml:value>066103</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="uupid">
+        <dsml:value>dfisher</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="middleName">
+        <dsml:value>W</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="givenName">
+        <dsml:value>Daniel</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="telephoneNumber">
+        <dsml:value>5402312096</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="displayName">
+        <dsml:value>Daniel W Fisher</dsml:value>
+      </dsml:attr>
+    </dsml:entry>
+  </dsml:directory-entries>
+</dsml:dsml>
diff --git a/src/test/resources/edu/vt/middleware/ldap/dfisher.dsmlv2 b/src/test/resources/edu/vt/middleware/ldap/dfisher.dsmlv2
new file mode 100644
index 0000000..885baf6
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/dfisher.dsmlv2
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<batchResponse xmlns="urn:oasis:names:tc:DSML:2:0:core">
+  <searchResponse>
+    <searchResultEntry dn="uid=818037,ou=People,dc=vt,dc=edu">
+      <attr name="objectClass">
+        <value>virginiaTechPerson</value>
+      </attr>
+      <attr name="title">
+        <value>Project Manager, Middleware</value>
+      </attr>
+      <attr name="uid">
+        <value>818037</value>
+      </attr>
+      <attr name="authId">
+        <value>dfisher</value>
+      </attr>
+      <attr name="mail">
+        <value>dfisher at vt.edu</value>
+      </attr>
+      <attr name="instantMessagingID">
+        <value>Yahoo!:dfish3r</value>
+        <value>Google:dfisher at gmail.com</value>
+        <value>Virginia Tech:dfisher at im.vt.edu</value>
+        <value>ICQ:8168282</value>
+      </attr>
+      <attr name="department">
+        <value>MiddleWare</value>
+      </attr>
+      <attr name="sn">
+        <value>Fisher</value>
+      </attr>
+      <attr name="userCertificate;binary">
+        <value encoding="base64">MIIC7zCCAligAwIBAgIQCBdZgzdx+yX5+jSRD50SJzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwNjI5MTg0NTQ2WhcNMTAwNjI5MTg0NTQ2WjBZMQ8wDQYDVQQEEwZGaXNoZXIxDzANBgNVBCoTBkRhbmllbDEWMBQGA1UEAxMNRGFuaWVsIEZpc2hlcjEdMBsGCSqGSIb3DQEJARYOZGZpc2hlckB2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbsC0qvAkU3oWwz39oq/ur/mhHZ+KNODug+pAsg0ZZbSnxWD9EVBiMere [...]
+      </attr>
+      <attr name="cn">
+        <value>Daniel W Fisher</value>
+      </attr>
+      <attr name="mailStop">
+        <value>0479</value>
+      </attr>
+      <attr name="suppressDisplay">
+        <value>false</value>
+      </attr>
+      <attr name="suppressedAttribute">
+        <value>localPostalAddress</value>
+        <value>homePostalAddress</value>
+        <value>homePhone</value>
+        <value>localPhone</value>
+      </attr>
+      <attr name="postalAddress">
+        <value>SETI-Middleware$1700 Pratt Drive$Blacksburg, VA 24060</value>
+      </attr>
+      <attr name="departmentNumber">
+        <value>066103</value>
+      </attr>
+      <attr name="uupid">
+        <value>dfisher</value>
+      </attr>
+      <attr name="middleName">
+        <value>W</value>
+      </attr>
+      <attr name="givenName">
+        <value>Daniel</value>
+      </attr>
+      <attr name="telephoneNumber">
+        <value>5402312096</value>
+      </attr>
+      <attr name="displayName">
+        <value>Daniel W Fisher</value>
+      </attr>
+    </searchResultEntry>
+    <searchResultDone>
+      <resultCode code="0"/>
+    </searchResultDone>
+  </searchResponse>
+</batchResponse>
diff --git a/src/test/resources/edu/vt/middleware/ldap/dfisher.ldif b/src/test/resources/edu/vt/middleware/ldap/dfisher.ldif
new file mode 100644
index 0000000..8d020bb
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/dfisher.ldif
@@ -0,0 +1,28 @@
+dn: uid=818037,ou=People,dc=vt,dc=edu
+middleName: W
+uid: 818037
+mail: dfisher at vt.edu
+sn: Fisher
+instantMessagingID: Yahoo!:dfish3r
+instantMessagingID: Google:dfisher at gmail.com
+instantMessagingID: Virginia Tech:dfisher at im.vt.edu
+instantMessagingID: ICQ:8168282
+department: MiddleWare
+objectClass: virginiaTechPerson
+suppressedAttribute: localPostalAddress
+suppressedAttribute: homePostalAddress
+suppressedAttribute: homePhone
+suppressedAttribute: localPhone
+givenName: Daniel
+title: Project Manager, Middleware
+mailStop: 0479
+departmentNumber: 066103
+cn: Daniel W Fisher
+authId: dfisher
+telephoneNumber: 5402312096
+userCertificate;binary:: MIIC7zCCAligAwIBAgIQCBdZgzdx+yX5+jSRD50SJzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwNjI5MTg0NTQ2WhcNMTAwNjI5MTg0NTQ2WjBZMQ8wDQYDVQQEEwZGaXNoZXIxDzANBgNVBCoTBkRhbmllbDEWMBQGA1UEAxMNRGFuaWVsIEZpc2hlcjEdMBsGCSqGSIb3DQEJARYOZGZpc2hlckB2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbsC0qvAkU3oWwz39oq/ur/mhHZ+KNODug+pAsg0ZZbSnxWD9EVBiMerevD5bJ0we [...]
+postalAddress: SETI-Middleware$1700 Pratt Drive$Blacksburg, VA 24060
+displayName: Daniel W Fisher
+suppressDisplay: false
+uupid: dfisher
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.dsmlv1 b/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.dsmlv1
new file mode 100644
index 0000000..457700a
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.dsmlv1
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<dsml:dsml xmlns:dsml="http://www.dsml.org/DSML">
+  <dsml:directory-entries>
+    <dsml:entry dn="uid=818037,ou=People,dc=vt,dc=edu">
+      <dsml:objectclass>
+        <dsml:oc-value>virginiaTechPerson</dsml:oc-value>
+      </dsml:objectclass>
+      <dsml:attr name="authId">
+        <dsml:value>dfisher</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="cn">
+        <dsml:value>Daniel W Fisher</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="department">
+        <dsml:value>MiddleWare</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="departmentNumber">
+        <dsml:value>066103</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="displayName">
+        <dsml:value>Daniel W Fisher</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="givenName">
+        <dsml:value>Daniel</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="instantMessagingID">
+        <dsml:value>Google:dfisher at gmail.com</dsml:value>
+        <dsml:value>ICQ:8168282</dsml:value>
+        <dsml:value>Virginia Tech:dfisher at im.vt.edu</dsml:value>
+        <dsml:value>Yahoo!:dfish3r</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="mail">
+        <dsml:value>dfisher at vt.edu</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="mailStop">
+        <dsml:value>0479</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="middleName">
+        <dsml:value>W</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="postalAddress">
+        <dsml:value>SETI-Middleware$1700 Pratt Drive$Blacksburg, VA 24060</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="sn">
+        <dsml:value>Fisher</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="suppressDisplay">
+        <dsml:value>false</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="suppressedAttribute">
+        <dsml:value>homePhone</dsml:value>
+        <dsml:value>homePostalAddress</dsml:value>
+        <dsml:value>localPhone</dsml:value>
+        <dsml:value>localPostalAddress</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="telephoneNumber">
+        <dsml:value>5402312096</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="title">
+        <dsml:value>Project Manager, Middleware</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="uid">
+        <dsml:value>818037</dsml:value>
+      </dsml:attr>
+      <dsml:attr name="userCertificate;binary">
+        <dsml:value encoding="base64">MIIC7zCCAligAwIBAgIQCBdZgzdx+yX5+jSRD50SJzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwNjI5MTg0NTQ2WhcNMTAwNjI5MTg0NTQ2WjBZMQ8wDQYDVQQEEwZGaXNoZXIxDzANBgNVBCoTBkRhbmllbDEWMBQGA1UEAxMNRGFuaWVsIEZpc2hlcjEdMBsGCSqGSIb3DQEJARYOZGZpc2hlckB2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbsC0qvAkU3oWwz39oq/ur/mhHZ+KNODug+pAsg0ZZbSnxWD9EVB [...]
+      </dsml:attr>
+      <dsml:attr name="uupid">
+        <dsml:value>dfisher</dsml:value>
+      </dsml:attr>
+    </dsml:entry>
+  </dsml:directory-entries>
+</dsml:dsml>
diff --git a/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.dsmlv2 b/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.dsmlv2
new file mode 100644
index 0000000..67e98f0
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.dsmlv2
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<batchResponse xmlns="urn:oasis:names:tc:DSML:2:0:core">
+  <searchResponse>
+    <searchResultEntry dn="uid=818037,ou=People,dc=vt,dc=edu">
+      <attr name="authId">
+        <value>dfisher</value>
+      </attr>
+      <attr name="cn">
+        <value>Daniel W Fisher</value>
+      </attr>
+      <attr name="department">
+        <value>MiddleWare</value>
+      </attr>
+      <attr name="departmentNumber">
+        <value>066103</value>
+      </attr>
+      <attr name="displayName">
+        <value>Daniel W Fisher</value>
+      </attr>
+      <attr name="givenName">
+        <value>Daniel</value>
+      </attr>
+      <attr name="instantMessagingID">
+        <value>Google:dfisher at gmail.com</value>
+        <value>ICQ:8168282</value>
+        <value>Virginia Tech:dfisher at im.vt.edu</value>
+        <value>Yahoo!:dfish3r</value>
+      </attr>
+      <attr name="mail">
+        <value>dfisher at vt.edu</value>
+      </attr>
+      <attr name="mailStop">
+        <value>0479</value>
+      </attr>
+      <attr name="middleName">
+        <value>W</value>
+      </attr>
+      <attr name="objectClass">
+        <value>virginiaTechPerson</value>
+      </attr>
+      <attr name="postalAddress">
+        <value>SETI-Middleware$1700 Pratt Drive$Blacksburg, VA 24060</value>
+      </attr>
+      <attr name="sn">
+        <value>Fisher</value>
+      </attr>
+      <attr name="suppressDisplay">
+        <value>false</value>
+      </attr>
+      <attr name="suppressedAttribute">
+        <value>homePhone</value>
+        <value>homePostalAddress</value>
+        <value>localPhone</value>
+        <value>localPostalAddress</value>
+      </attr>
+      <attr name="telephoneNumber">
+        <value>5402312096</value>
+      </attr>
+      <attr name="title">
+        <value>Project Manager, Middleware</value>
+      </attr>
+      <attr name="uid">
+        <value>818037</value>
+      </attr>
+      <attr name="userCertificate;binary">
+        <value encoding="base64">MIIC7zCCAligAwIBAgIQCBdZgzdx+yX5+jSRD50SJzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwNjI5MTg0NTQ2WhcNMTAwNjI5MTg0NTQ2WjBZMQ8wDQYDVQQEEwZGaXNoZXIxDzANBgNVBCoTBkRhbmllbDEWMBQGA1UEAxMNRGFuaWVsIEZpc2hlcjEdMBsGCSqGSIb3DQEJARYOZGZpc2hlckB2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbsC0qvAkU3oWwz39oq/ur/mhHZ+KNODug+pAsg0ZZbSnxWD9EVBiMere [...]
+      </attr>
+      <attr name="uupid">
+        <value>dfisher</value>
+      </attr>
+    </searchResultEntry>
+    <searchResultDone>
+      <resultCode code="0"/>
+    </searchResultDone>
+  </searchResponse>
+</batchResponse>
diff --git a/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.ldif b/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.ldif
new file mode 100644
index 0000000..352d117
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/dfisher.sorted.ldif
@@ -0,0 +1,28 @@
+dn: uid=818037,ou=People,dc=vt,dc=edu
+authId: dfisher
+cn: Daniel W Fisher
+department: MiddleWare
+departmentNumber: 066103
+displayName: Daniel W Fisher
+givenName: Daniel
+instantMessagingID: Google:dfisher at gmail.com
+instantMessagingID: ICQ:8168282
+instantMessagingID: Virginia Tech:dfisher at im.vt.edu
+instantMessagingID: Yahoo!:dfish3r
+mail: dfisher at vt.edu
+mailStop: 0479
+middleName: W
+objectClass: virginiaTechPerson
+postalAddress: SETI-Middleware$1700 Pratt Drive$Blacksburg, VA 24060
+sn: Fisher
+suppressDisplay: false
+suppressedAttribute: homePhone
+suppressedAttribute: homePostalAddress
+suppressedAttribute: localPhone
+suppressedAttribute: localPostalAddress
+telephoneNumber: 5402312096
+title: Project Manager, Middleware
+uid: 818037
+userCertificate;binary:: MIIC7zCCAligAwIBAgIQCBdZgzdx+yX5+jSRD50SJzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwNjI5MTg0NTQ2WhcNMTAwNjI5MTg0NTQ2WjBZMQ8wDQYDVQQEEwZGaXNoZXIxDzANBgNVBCoTBkRhbmllbDEWMBQGA1UEAxMNRGFuaWVsIEZpc2hlcjEdMBsGCSqGSIb3DQEJARYOZGZpc2hlckB2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbsC0qvAkU3oWwz39oq/ur/mhHZ+KNODug+pAsg0ZZbSnxWD9EVBiMerevD5bJ0we [...]
+uupid: dfisher
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/getSchemaResults.ldif b/src/test/resources/edu/vt/middleware/ldap/getSchemaResults.ldif
new file mode 100644
index 0000000..f9bae17
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/getSchemaResults.ldif
@@ -0,0 +1,12 @@
+dn: AttributeDefinition
+objectclass: AttributeDefinition
+
+dn: ClassDefinition
+objectclass: ClassDefinition
+
+dn: MatchingRule
+objectclass: MatchingRule
+
+dn: SyntaxDefinition
+objectclass: SyntaxDefinition
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/image.jpg b/src/test/resources/edu/vt/middleware/ldap/image.jpg
new file mode 100644
index 0000000..08404f1
Binary files /dev/null and b/src/test/resources/edu/vt/middleware/ldap/image.jpg differ
diff --git a/src/test/resources/edu/vt/middleware/ldap/mergeDuplicateResults.ldif b/src/test/resources/edu/vt/middleware/ldap/mergeDuplicateResults.ldif
new file mode 100644
index 0000000..46d36fb
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/mergeDuplicateResults.ldif
@@ -0,0 +1,19 @@
+dn: uugid=group3,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+creationDate: 2010-06-06T12:00:00
+creationDate: 2010-06-06T12:00:00
+member: uugid=group3,ou=test,dc=vt,dc=edu
+member: uugid=group4,ou=test,dc=vt,dc=edu
+member: uugid=group5,ou=test,dc=vt,dc=edu
+objectClass: virginiaTechGroup
+objectClass: virginiaTechGroup
+objectClass: virginiaTechGroup
+uid: 2003
+uid: 2004
+uid: 2005
+uugid: group3
+uugid: group4
+uugid: group5
diff --git a/src/test/resources/edu/vt/middleware/ldap/mergeResults.ldif b/src/test/resources/edu/vt/middleware/ldap/mergeResults.ldif
new file mode 100644
index 0000000..f02c509
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/mergeResults.ldif
@@ -0,0 +1,13 @@
+dn: uugid=group3,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+member: uugid=group3,ou=test,dc=vt,dc=edu
+member: uugid=group4,ou=test,dc=vt,dc=edu
+member: uugid=group5,ou=test,dc=vt,dc=edu
+objectClass: virginiaTechGroup
+uid: 2003
+uid: 2004
+uid: 2005
+uugid: group3
+uugid: group4
+uugid: group5
diff --git a/src/test/resources/edu/vt/middleware/ldap/multipleEntriesIn.ldif b/src/test/resources/edu/vt/middleware/ldap/multipleEntriesIn.ldif
new file mode 100644
index 0000000..2785b43
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/multipleEntriesIn.ldif
@@ -0,0 +1,102 @@
+version: 1
+dn: uid=818037,ou=People,dc=vt,dc=edu
+middleName: W
+uid: 818037
+mail: dfisher at vt.edu
+sn: Fisher
+instantMessagingID: Yahoo!:dfish3r
+instantMessagingID: Google:dfisher at gmail.com
+instantMessagingID: Virginia Tech:dfisher at im.vt.edu
+instantMessagingID: ICQ:8168282
+department: MiddleWare
+objectClass: virginiaTechPerson
+# random comment
+suppressedAttribute: localPostalAddress
+suppressedAttribute: homePostalAddress
+# random comment
+suppressedAttribute: homePhone
+suppressedAttribute: localPhone
+givenName: Daniel
+title: Project Manager, Middleware
+mailStop: 0479
+departmentNumber: 066103
+cn: Daniel W Fisher
+authId: dfisher
+telephoneNumber: 5402312096
+userCertificate;binary:: MIIC7zCCAligAwIBAgIQCBdZgzdx+yX5+jSRD50SJzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwNjI5MTg0NTQ2WhcNMTAwNjI5MTg0NTQ2WjBZMQ8wDQYDVQQEEwZGaXNoZXIxDzANBgNVBCoTBkRhbmllbDEWMBQGA1UEAxMNRGFuaWVsIEZpc2hlcjEdMBsGCSqGSIb3DQEJARYOZGZpc2hlckB2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbsC0qvAkU3oWwz39oq/ur/mhHZ+KNODug+pAsg0ZZbSnxWD9EVBiMerevD5bJ0we [...]
+postalAddress: SETI-Middleware 
+ 1700 Pratt Drive 
+ Blacksburg, VA 24060
+displayName: Daniel W Fisher
+suppressDisplay: false
+jpegPhoto:< file:///${basedir}/src/test/resources/edu/vt/middleware/ldap/image.jpg
+uupid: dfisher
+
+# random comment
+
+dn:uid=1152120,ou=People,dc=vt,dc=edu
+middleName:H
+uid:1152120
+mail:dhawes at vt.edu
+sn:Hawes
+instantMessagingID:Google:dhawes at gmail.com
+instantMessagingID:Virginia Tech:dhawes at im.iad.vt.edu
+department:MiddleWare
+objectClass:virginiaTechPerson
+suppressedAttribute:localPostalAddress
+# random comment
+givenName:David
+labeledURI:Homepage:http://filebox.vt.edu/users/dhawes
+title:Middleware Application Developer
+mailStop:0479
+localPhone:5405529055
+departmentNumber:066103
+cn:David H Hawes
+authId:dhawes
+telephoneNumber:5402316978
+# random comment
+postalAddress:SETI-Middleware$1700 Pratt Dr.$Blacksburg, VA 24061
+displayName:David H Hawes
+suppressDisplay:false
+uupid:dhawes
+
+dn: uid=1145718,ou=People,dc=vt,dc=edu
+middleName: S
+uid: 1145718
+mail: serac at vt.edu
+sn: Addison
+instantMessagingID: Virginia Tech:serac at im.vt.edu
+department: MiddleWare
+objectClass: virginiaTechPerson
+suppressedAttribute: localPostalAddress
+givenName: Marvin
+labeledURI: Homepage:http://www.middleware.vt.edu/doku.php?id=middleware:serac
+title: IT Specialist
+mailStop: 0479
+departmentNumber: 066103
+cn: Marvin S Addison
+authId: serac
+telephoneNumber: 5402317050
+# random comment
+userCertificate;binary:: MIIC/TCCAmagAwIBAgIQKQxJ2CQ0bujrfVdU9505nzANB
+ gkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHR
+ pbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc
+ 3VpbmcgQ0EwHhcNMDkwODI3MjAwNzExWhcNMTAwODI3MjAwNzExWjBZMRAwDgYDVQQEEwd
+ BZGRpc29uMQ8wDQYDVQQqEwZNYXJ2aW4xFzAVBgNVBAMTDk1hcnZpbiBBZGRpc29uMRswG
+ QYJKoZIhvcNAQkBFgxzZXJhY0B2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggE
+ KAoIBAQCYU7i0HPyrjhLdm09uprGeIJQnkdxU+HCu1oT0SPVg6I0/KYw64bbKSc36cHE5d
+ 03RpqkU5HCSJ5m4OaluDYezaOz6TcSUhAAmYnkF8lCXI7zBS1VGlPe2w3ronZoBb6bZ0Po
+ bBpD+1AdRRfV+6FNqhnqG+ZAOlEsAwyUkEd/lTPjYsS7Te0GXOyhPTDUiKixBdPSuMSuAJ
+ JZKrAIBTRe62dpDSOvBYJSVVpuGdiCSaG1YQWI3DkvjzLT4+0zvDPjb7dvePnbkbpwyFfK
+ +e1wYpK2yShHiJeOPAdVUZ+PjeBFnD2TkDgk/Tf8j6QJLMw0Apt7rATxW9NAUEPLoPeCFA
+ gMBAAGjOTA3MA4GA1UdDwEB/wQEAwID+DAXBgNVHREEEDAOgQxzZXJhY0B2dC5lZHUwDAY
+ DVR0TAQH/BAIwADANBgkqhkiG9w0BAQUFAAOBgQC5V1Kb0q7ajm7OZjbjv8xosxblvt9Qh
+ l5yBhnX4xL9QomntH60LZc2pf20hnJ6Pl6Onxk4LejTtemyL0Bm0i3DJbqCCBFaOsEdX2G
+ 3D3HPNhdphYckn7fjzuvUudMEkfvrPDj0evdJIgwOXDEvsh8y8LggO1Bp6xj63H0oPePHB
+ w==
+postalAddress: SETI-Middleware 1700 Pratt Drive 
+ Room 111A Blacksburg, VA 24060
+displayName: Marvin S Addison
+suppressDisplay: false
+uupid: serac
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/multipleEntriesOut.ldif b/src/test/resources/edu/vt/middleware/ldap/multipleEntriesOut.ldif
new file mode 100644
index 0000000..da35895
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/multipleEntriesOut.ldif
@@ -0,0 +1,76 @@
+dn: uid=818037,ou=People,dc=vt,dc=edu
+middleName: W
+uid: 818037
+mail: dfisher at vt.edu
+sn: Fisher
+instantMessagingID: Yahoo!:dfish3r
+instantMessagingID: Google:dfisher at gmail.com
+instantMessagingID: Virginia Tech:dfisher at im.vt.edu
+instantMessagingID: ICQ:8168282
+department: MiddleWare
+objectClass: virginiaTechPerson
+suppressedAttribute: localPostalAddress
+suppressedAttribute: homePostalAddress
+suppressedAttribute: homePhone
+suppressedAttribute: localPhone
+givenName: Daniel
+title: Project Manager, Middleware
+mailStop: 0479
+departmentNumber: 066103
+cn: Daniel W Fisher
+authId: dfisher
+telephoneNumber: 5402312096
+userCertificate;binary:: MIIC7zCCAligAwIBAgIQCBdZgzdx+yX5+jSRD50SJzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwNjI5MTg0NTQ2WhcNMTAwNjI5MTg0NTQ2WjBZMQ8wDQYDVQQEEwZGaXNoZXIxDzANBgNVBCoTBkRhbmllbDEWMBQGA1UEAxMNRGFuaWVsIEZpc2hlcjEdMBsGCSqGSIb3DQEJARYOZGZpc2hlckB2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbsC0qvAkU3oWwz39oq/ur/mhHZ+KNODug+pAsg0ZZbSnxWD9EVBiMerevD5bJ0we [...]
+postalAddress: SETI-Middleware 1700 Pratt Drive Blacksburg, VA 24060
+displayName: Daniel W Fisher
+suppressDisplay: false
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+uupid: dfisher
+
+dn: uid=1152120,ou=People,dc=vt,dc=edu
+middleName: H
+uid: 1152120
+mail: dhawes at vt.edu
+sn: Hawes
+instantMessagingID: Google:dhawes at gmail.com
+instantMessagingID: Virginia Tech:dhawes at im.iad.vt.edu
+department: MiddleWare
+objectClass: virginiaTechPerson
+suppressedAttribute: localPostalAddress
+givenName: David
+labeledURI: Homepage:http://filebox.vt.edu/users/dhawes
+title: Middleware Application Developer
+mailStop: 0479
+localPhone: 5405529055
+departmentNumber: 066103
+cn: David H Hawes
+authId: dhawes
+telephoneNumber: 5402316978
+postalAddress: SETI-Middleware$1700 Pratt Dr.$Blacksburg, VA 24061
+displayName: David H Hawes
+suppressDisplay: false
+uupid: dhawes
+
+dn: uid=1145718,ou=People,dc=vt,dc=edu
+middleName: S
+uid: 1145718
+mail: serac at vt.edu
+sn: Addison
+instantMessagingID: Virginia Tech:serac at im.vt.edu
+department: MiddleWare
+objectClass: virginiaTechPerson
+suppressedAttribute: localPostalAddress
+givenName: Marvin
+labeledURI: Homepage:http://www.middleware.vt.edu/doku.php?id=middleware:serac
+title: IT Specialist
+mailStop: 0479
+departmentNumber: 066103
+cn: Marvin S Addison
+authId: serac
+telephoneNumber: 5402317050
+userCertificate;binary:: MIIC/TCCAmagAwIBAgIQKQxJ2CQ0bujrfVdU9505nzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElzc3VpbmcgQ0EwHhcNMDkwODI3MjAwNzExWhcNMTAwODI3MjAwNzExWjBZMRAwDgYDVQQEEwdBZGRpc29uMQ8wDQYDVQQqEwZNYXJ2aW4xFzAVBgNVBAMTDk1hcnZpbiBBZGRpc29uMRswGQYJKoZIhvcNAQkBFgxzZXJhY0B2dC5lZHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYU7i0HPyrjhLdm09uprGeIJQnkdxU+HCu1oT0SPVg6I0/KYw64bbKSc36cHE5d03 [...]
+postalAddress: SETI-Middleware 1700 Pratt Drive Room 111A Blacksburg, VA 24060
+displayName: Marvin S Addison
+suppressDisplay: false
+uupid: serac
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/pagedResults.ldif b/src/test/resources/edu/vt/middleware/ldap/pagedResults.ldif
new file mode 100644
index 0000000..011fb01
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/pagedResults.ldif
@@ -0,0 +1,32 @@
+dn: uugid=group2,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+member: uugid=group3,ou=test,dc=vt,dc=edu
+uid: 2002
+uugid: group2
+
+dn: uugid=group3,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+member: uugid=group4,ou=test,dc=vt,dc=edu
+member: uugid=group5,ou=test,dc=vt,dc=edu
+uid: 2003
+uugid: group3
+
+dn: uugid=group4,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+member: uugid=group3,ou=test,dc=vt,dc=edu
+uid: 2004
+uugid: group4
+
+dn: uugid=group5,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+objectClass: virginiaTechGroup
+uid: 2005
+uugid: group5
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/recursiveAttributeHandlerResults.ldif b/src/test/resources/edu/vt/middleware/ldap/recursiveAttributeHandlerResults.ldif
new file mode 100644
index 0000000..c4bb5a2
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/recursiveAttributeHandlerResults.ldif
@@ -0,0 +1,9 @@
+dn: uugid=group2,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+member: uugid=group3,ou=test,dc=vt,dc=edu
+member: uugid=group4,ou=test,dc=vt,dc=edu
+member: uugid=group5,ou=test,dc=vt,dc=edu
+objectClass: virginiaTechGroup
+uid: 2002
+uugid: group2
diff --git a/src/test/resources/edu/vt/middleware/ldap/recursiveSearchResultHandlerResults.ldif b/src/test/resources/edu/vt/middleware/ldap/recursiveSearchResultHandlerResults.ldif
new file mode 100644
index 0000000..d387d7b
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/recursiveSearchResultHandlerResults.ldif
@@ -0,0 +1,13 @@
+dn: uugid=group2,ou=test,dc=vt,dc=edu
+contactPerson: uid=2,ou=test,dc=vt,dc=edu
+creationDate: 2010-06-06T12:00:00
+member: uugid=group3,ou=test,dc=vt,dc=edu
+objectClass: virginiaTechGroup
+uid: 2002
+uid: 2003
+uid: 2004
+uid: 2005
+uugid: group2
+uugid: group3
+uugid: group4
+uugid: group5
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchAttributesResults-2.ldif b/src/test/resources/edu/vt/middleware/ldap/searchAttributesResults-2.ldif
new file mode 100644
index 0000000..739cba5
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchAttributesResults-2.ldif
@@ -0,0 +1,5 @@
+dn: uid=2,ou=test,dc=vt,dc=edu
+departmentNumber: 0822
+givenName: John
+sn: Doe
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-10.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-10.ldif
new file mode 100644
index 0000000..7f400ea
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-10.ldif
@@ -0,0 +1,5 @@
+dn: uid=10,ou=test,dc=vt,dc=edu
+departmentNumber: 0830
+givenName: Johv
+sn: Dom
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-12.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-12.ldif
new file mode 100644
index 0000000..e137f35
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-12.ldif
@@ -0,0 +1,15 @@
+dn: uid=12,ou=test,dc=vt,dc=edu
+cn: Johx H Doo
+departmentNumber: 0830
+displayName: Johx H Doo
+givenName: Johx
+initials: JHD
+mail: jdoe12 at vt.edu
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Doo
+uid: 12
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-2.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-2.ldif
new file mode 100644
index 0000000..739cba5
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-2.ldif
@@ -0,0 +1,5 @@
+dn: uid=2,ou=test,dc=vt,dc=edu
+departmentNumber: 0822
+givenName: John
+sn: Doe
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-3.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-3.ldif
new file mode 100644
index 0000000..d4d594c
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-3.ldif
@@ -0,0 +1,5 @@
+dn: uid=3,ou=test,dc=vt,dc=edu
+departmentNumber: 0823
+givenName: Joho
+sn: Dof
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-4.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-4.ldif
new file mode 100644
index 0000000..118d97f
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-4.ldif
@@ -0,0 +1,5 @@
+dn: uid=4,ou=test,dc=vt,dc=edu
+departmentNumber: 0824
+givenName: Johp
+sn: Dog
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-5.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-5.ldif
new file mode 100644
index 0000000..d69d450
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-5.ldif
@@ -0,0 +1,5 @@
+dn: uid=5,ou=test,dc=vt,dc=edu
+departmentNumber: 0825
+givenName: Johq
+sn: Doh
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-6.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-6.ldif
new file mode 100644
index 0000000..ee4c127
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-6.ldif
@@ -0,0 +1,5 @@
+dn: uid=6,ou=test,dc=vt,dc=edu
+departmentNumber: 0826
+givenName: Johr
+sn: Doi
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-7.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-7.ldif
new file mode 100644
index 0000000..46a3321
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-7.ldif
@@ -0,0 +1,5 @@
+dn: uid=7,ou=test,dc=vt,dc=edu
+departmentNumber: 0827
+givenName: Johs
+sn: Doj
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-8.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-8.ldif
new file mode 100644
index 0000000..d737f47
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-8.ldif
@@ -0,0 +1,6 @@
+dn: uid=8,ou=test,dc=vt,dc=edu
+departmentNumber: 0828
+givenName: Joht
+sn: Dok
+jpegPhoto:: /9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPRTXkZ7wHW [...]
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/searchResults-9.ldif b/src/test/resources/edu/vt/middleware/ldap/searchResults-9.ldif
new file mode 100644
index 0000000..739240f
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/searchResults-9.ldif
@@ -0,0 +1,5 @@
+dn: uid=9,ou=test,dc=vt,dc=edu
+departmentNumber: 0829
+givenName: Johu
+sn: Dol
+
diff --git a/src/test/resources/edu/vt/middleware/ldap/specialChars-2.ldif b/src/test/resources/edu/vt/middleware/ldap/specialChars-2.ldif
new file mode 100644
index 0000000..8ccfaa3
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/specialChars-2.ldif
@@ -0,0 +1,10 @@
+dn: cn=Test\/User\/ \#2\,3\=4\/\+5\<6\>7\;8,ou=test,dc=vt,dc=edu
+cn: Test/User/ #2,3=4/+5<6>7;8
+givenName: Test
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: User
+uid: 17894
+userPassword: {SHA}KqYKj/f81HPTIeAUav2eJt85UUc=
diff --git a/src/test/resources/edu/vt/middleware/ldap/specialChars.ldif b/src/test/resources/edu/vt/middleware/ldap/specialChars.ldif
new file mode 100644
index 0000000..7ee2123
--- /dev/null
+++ b/src/test/resources/edu/vt/middleware/ldap/specialChars.ldif
@@ -0,0 +1,9 @@
+dn: cn=Test\/User\/ \#1,ou=test,dc=vt,dc=edu
+cn: Test/User/ #1
+givenName: Test
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: User
+uid: 17893
diff --git a/src/test/resources/krb5.keytab b/src/test/resources/krb5.keytab
new file mode 100644
index 0000000..c800daa
Binary files /dev/null and b/src/test/resources/krb5.keytab differ
diff --git a/src/test/resources/ldap.conn.properties b/src/test/resources/ldap.conn.properties
new file mode 100644
index 0000000..1e52a6a
--- /dev/null
+++ b/src/test/resources/ldap.conn.properties
@@ -0,0 +1,30 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://dne.middleware.vt.edu ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+edu.vt.middleware.ldap.bindDn=uid=1,ou=test,dc=vt,dc=edu
+
+# credential for the bind DN
+edu.vt.middleware.ldap.bindCredential=VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2
+
+# LDAP authentication mechanism
+# default value is 'simple'
+edu.vt.middleware.ldap.authtype=simple
+
+# sets the search scope
+# default value is 'SUBTREE'
+edu.vt.middleware.ldap.searchScope=SUBTREE
+
+# set socket timeout low for testing
+edu.vt.middleware.ldap.timeout=2000
+
+edu.vt.middleware.ldap.connectionHandler=edu.vt.middleware.ldap.handler.DefaultConnectionHandler{{connectionStrategy=ROUND_ROBIN}}
diff --git a/src/test/resources/ldap.cram-md5.properties b/src/test/resources/ldap.cram-md5.properties
new file mode 100644
index 0000000..214ec00
--- /dev/null
+++ b/src/test/resources/ldap.cram-md5.properties
@@ -0,0 +1,18 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:389 ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# set socket timeout low for testing
+edu.vt.middleware.ldap.timeout=2000
+
+# LDAP authentication mechanism
+# default value is 'simple'
+edu.vt.middleware.ldap.authtype=CRAM-MD5
diff --git a/src/test/resources/ldap.digest-md5.properties b/src/test/resources/ldap.digest-md5.properties
new file mode 100644
index 0000000..34a2898
--- /dev/null
+++ b/src/test/resources/ldap.digest-md5.properties
@@ -0,0 +1,18 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:389 ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# set socket timeout low for testing
+edu.vt.middleware.ldap.timeout=2000
+
+# LDAP authentication mechanism
+# default value is 'simple'
+edu.vt.middleware.ldap.authtype=DIGEST-MD5
diff --git a/src/test/resources/ldap.gssapi.properties b/src/test/resources/ldap.gssapi.properties
new file mode 100644
index 0000000..bd56713
--- /dev/null
+++ b/src/test/resources/ldap.gssapi.properties
@@ -0,0 +1,19 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# LDAP authentication mechanism
+# default value is 'simple'
+edu.vt.middleware.ldap.authtype=GSSAPI
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.tls=false
diff --git a/src/test/resources/ldap.null.properties b/src/test/resources/ldap.null.properties
new file mode 100644
index 0000000..2fb90e1
--- /dev/null
+++ b/src/test/resources/ldap.null.properties
@@ -0,0 +1,52 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+edu.vt.middleware.ldap.bindDn=uid=1,ou=test,dc=vt,dc=edu
+
+# credential for the bind DN
+edu.vt.middleware.ldap.bindCredential=VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2
+
+# LDAP authentication mechanism
+# default value is 'simple'
+edu.vt.middleware.ldap.authtype=simple
+
+# sets the batch size to use when returning results
+# default value is '-1'
+edu.vt.middleware.ldap.batchSize=-1
+
+# sets the search scope
+# default value is 'SUBTREE'
+edu.vt.middleware.ldap.searchScope=SUBTREE
+
+# sets the length of time that search operations will block
+# default value is 0, block forever
+edu.vt.middleware.ldap.timeLimit=5000
+
+# set socket timeout low for testing
+edu.vt.middleware.ldap.timeout=8000
+
+# specifies additional attributes which should be treated as binary
+# attribute names should be space delimited
+edu.vt.middleware.ldap.binaryAttributes=jpegPhoto
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.tls=false
+
+# nullable properties
+edu.vt.middleware.ldap.sslSocketFactory=null
+edu.vt.middleware.ldap.hostnameVerifier=null
+edu.vt.middleware.ldap.operationRetryExceptions=null
+edu.vt.middleware.ldap.searchResultHandlers=null
+edu.vt.middleware.ldap.handlerIgnoreExceptions=null
+
diff --git a/src/test/resources/ldap.parser.properties b/src/test/resources/ldap.parser.properties
new file mode 100644
index 0000000..290af0d
--- /dev/null
+++ b/src/test/resources/ldap.parser.properties
@@ -0,0 +1,50 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+edu.vt.middleware.ldap.bindDn=uid=1,ou=test,dc=vt,dc=edu
+
+# credential for the bind DN
+edu.vt.middleware.ldap.bindCredential=VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2
+
+# LDAP authentication mechanism
+# default value is 'simple'
+edu.vt.middleware.ldap.authtype=simple
+
+# sets the batch size to use when returning results
+# default value is '-1'
+edu.vt.middleware.ldap.batchSize=10
+
+# sets the search scope
+# default value is 'SUBTREE'
+edu.vt.middleware.ldap.searchScope=OBJECT
+
+# sets the length of time that search operations will block
+# default value is 0, block forever
+edu.vt.middleware.ldap.timeLimit=5000
+
+# set socket timeout low for testing
+edu.vt.middleware.ldap.timeout=8000
+
+# specifies additional attributes which should be treated as binary
+# attribute names should be space delimited
+edu.vt.middleware.ldap.binaryAttributes=jpegPhoto
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.tls=false
+
+# search result handlers
+edu.vt.middleware.ldap.searchResultHandlers=edu.vt.middleware.ldap.handler.RecursiveSearchResultHandler{{searchAttribute=member}{mergeAttributes=mail,department}},edu.vt.middleware.ldap.handler.MergeSearchResultHandler{{allowDuplicates=true}},edu.vt.middleware.ldap.handler.BinarySearchResultHandler{ },edu.vt.middleware.ldap.handler.EntryDnSearchResultHandler{{dnAttributeName=myDN}}
+
+# exceptions to ignore when searching
+edu.vt.middleware.ldap.handlerIgnoreExceptions=javax.naming.SizeLimitExceededException,javax.naming.PartialResultException
diff --git a/src/test/resources/ldap.pool.properties b/src/test/resources/ldap.pool.properties
new file mode 100644
index 0000000..6ad8178
--- /dev/null
+++ b/src/test/resources/ldap.pool.properties
@@ -0,0 +1,10 @@
+# Configuration variables for ldap pool operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+# period that the prune timer task runs
+edu.vt.middleware.ldap.pool.pruneTimerPeriod=5000
+
+# expiration time for objects in the ldap
+edu.vt.middleware.ldap.pool.expirationTime=1000
+
diff --git a/src/test/resources/ldap.properties b/src/test/resources/ldap.properties
new file mode 100644
index 0000000..eadea32
--- /dev/null
+++ b/src/test/resources/ldap.properties
@@ -0,0 +1,47 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+edu.vt.middleware.ldap.bindDn=uid=1,ou=test,dc=vt,dc=edu
+
+# credential for the bind DN
+edu.vt.middleware.ldap.bindCredential=VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2
+
+# LDAP authentication mechanism
+# default value is 'simple'
+edu.vt.middleware.ldap.authtype=simple
+
+# sets the batch size to use when returning results
+# default value is '-1'
+edu.vt.middleware.ldap.batchSize=-1
+
+# sets the search scope
+# default value is 'SUBTREE'
+edu.vt.middleware.ldap.searchScope=SUBTREE
+
+# sets the length of time that search operations will block
+# default value is 0, block forever
+edu.vt.middleware.ldap.timeLimit=5000
+
+# set socket timeout low for testing
+edu.vt.middleware.ldap.timeout=8000
+
+# specifies additional attributes which should be treated as binary
+# attribute names should be space delimited
+edu.vt.middleware.ldap.binaryAttributes=jpegPhoto
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.tls=false
+
+# exceptions to ignore when searching
+edu.vt.middleware.ldap.handlerIgnoreExceptions=javax.naming.SizeLimitExceededException,javax.naming.PartialResultException
diff --git a/src/test/resources/ldap.sasl.properties b/src/test/resources/ldap.sasl.properties
new file mode 100644
index 0000000..fa78a03
--- /dev/null
+++ b/src/test/resources/ldap.sasl.properties
@@ -0,0 +1,31 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# fully qualified class name which implements javax.net.ssl.SSLSocketFactory
+edu.vt.middleware.ldap.sslSocketFactory=edu.vt.middleware.ldap.ssl.TLSSocketFactory{ edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{ {trustStore=classpath:/ed.truststore} {trustStoreType=BKS} {keyStore=classpath:/ed.keystore} {keyStoreType=BKS} {keyStorePassword=changeit} }}
+
+# fully qualified class name which implements javax.net.ssl.HostnameVerifier
+edu.vt.middleware.ldap.hostnameVerifier=edu.vt.middleware.ldap.AnyHostnameVerifier
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:389 ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# LDAP authentication mechanism
+# default value is 'simple'
+edu.vt.middleware.ldap.authtype=EXTERNAL
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.tls=true
+
+# set socket timeout low for testing
+edu.vt.middleware.ldap.timeout=2000
+
+# LDAP field which contains user identifier
+edu.vt.middleware.ldap.auth.userField=uid,mail
diff --git a/src/test/resources/ldap.setup.properties b/src/test/resources/ldap.setup.properties
new file mode 100644
index 0000000..55b8b27
--- /dev/null
+++ b/src/test/resources/ldap.setup.properties
@@ -0,0 +1,17 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+edu.vt.middleware.ldap.bindDn=uid=1,ou=test,dc=vt,dc=edu
+
+# credential for the bind DN
+edu.vt.middleware.ldap.bindCredential=VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2
diff --git a/src/test/resources/ldap.ssl.properties b/src/test/resources/ldap.ssl.properties
new file mode 100644
index 0000000..b7da9b6
--- /dev/null
+++ b/src/test/resources/ldap.ssl.properties
@@ -0,0 +1,27 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# fully qualified class name which implements javax.net.ssl.SSLSocketFactory
+edu.vt.middleware.ldap.sslSocketFactory=edu.vt.middleware.ldap.ssl.SingletonTLSSocketFactory{edu.vt.middleware.ldap.ssl.X509CredentialConfig{{trustCertificates=file:src/test/resources/ed.trust.crt}}}
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:10636
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+edu.vt.middleware.ldap.bindDn=uid=1,ou=test,dc=vt,dc=edu
+
+# credential for the bind DN
+edu.vt.middleware.ldap.bindCredential=VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2
+
+# whether SSL should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.ssl=true
+
+# LDAP field which contains user identifier
+edu.vt.middleware.ldap.auth.userField=uid,mail
diff --git a/src/test/resources/ldap.tls.load.properties b/src/test/resources/ldap.tls.load.properties
new file mode 100644
index 0000000..75a0455
--- /dev/null
+++ b/src/test/resources/ldap.tls.load.properties
@@ -0,0 +1,30 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# fully qualified class name which implements javax.net.ssl.SSLSocketFactory
+edu.vt.middleware.ldap.sslSocketFactory={ edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{ {trustStore=classpath:/ed.truststore} {trustStoreType=BKS} }}
+
+# fully qualified class name which implements javax.net.ssl.HostnameVerifier
+edu.vt.middleware.ldap.hostnameVerifier=edu.vt.middleware.ldap.AnyHostnameVerifier
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=ou=test,dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+edu.vt.middleware.ldap.bindDn=uid=1,ou=test,dc=vt,dc=edu
+
+# credential for the bind DN
+edu.vt.middleware.ldap.bindCredential=VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.tls=true
+
+# LDAP field which contains user identifier
+edu.vt.middleware.ldap.auth.userField=uid,mail
diff --git a/src/test/resources/ldap.tls.properties b/src/test/resources/ldap.tls.properties
new file mode 100644
index 0000000..6189b2e
--- /dev/null
+++ b/src/test/resources/ldap.tls.properties
@@ -0,0 +1,43 @@
+# Configuration variables for ldap operation
+# Comments must be on separate lines
+# Format is 'name=value'
+
+## LDAP CONFIG ##
+
+# fully qualified class name which implements javax.net.ssl.SSLSocketFactory
+edu.vt.middleware.ldap.sslSocketFactory={ trustCertificates=file:src/test/resources/ed.trust.crt }
+
+# fully qualified class name which implements javax.net.ssl.SSLSocketFactory
+#edu.vt.middleware.ldap.auth.sslSocketFactory=edu.vt.middleware.ldap.ssl.TLSSocketFactory{enabledCipherSuites=TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA}
+
+# fully qualified class name which implements javax.net.ssl.HostnameVerifier
+edu.vt.middleware.ldap.hostnameVerifier=edu.vt.middleware.ldap.AnyHostnameVerifier
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:389
+
+# hostname of the LDAP
+edu.vt.middleware.ldap.auth.ldapUrl=ldap://ldap-test-1.middleware.vt.edu:389 ldap://ldap-test-1.middleware.vt.edu:10389
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.auth.baseDn=ou=test,dc=vt,dc=edu
+
+# base dn for performing user lookups
+edu.vt.middleware.ldap.baseDn=dc=vt,dc=edu
+
+# bind DN if one is required to bind before searching
+edu.vt.middleware.ldap.bindDn=uid=1,ou=test,dc=vt,dc=edu
+
+# credential for the bind DN
+edu.vt.middleware.ldap.bindCredential=VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2
+
+# whether TLS should be used for LDAP connections
+# default value is 'false'
+edu.vt.middleware.ldap.tls=true
+
+# set socket timeout low for testing
+edu.vt.middleware.ldap.timeout=2000
+
+# LDAP field which contains user identifier
+edu.vt.middleware.ldap.auth.userField=uid,mail
+
diff --git a/src/test/resources/ldap_jaas.config b/src/test/resources/ldap_jaas.config
new file mode 100644
index 0000000..f1b764e
--- /dev/null
+++ b/src/test/resources/ldap_jaas.config
@@ -0,0 +1,279 @@
+vt-ldap {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier{foo=test}"
+    operationRetryException="javax.naming.CommunicationException,javax.naming.ServiceUnavailableException"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="{trustCertificates=classpath:/ed.trust.crt}";
+};
+
+vt-ldap-ssl {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    ldapUrl="ldaps://ldap-test-1.middleware.vt.edu:10636"
+    baseDn="ou=test,dc=vt,dc=edu"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber";
+};
+
+vt-ldap-ssl-2 {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10636"
+    ssl="true"
+    baseDn="ou=test,dc=vt,dc=edu"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.SingletonTLSSocketFactory{edu.vt.middleware.ldap.ssl.X509CredentialConfig{{trustCertificates=classpath:/ed.trust.crt}}}";
+};
+
+vt-ldap-authz {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    connectionHandler="edu.vt.middleware.ldap.handler.TlsConnectionHandler"
+    authenticationHandler="edu.vt.middleware.ldap.auth.handler.BindAuthenticationHandler"
+    dnResolver="edu.vt.middleware.ldap.auth.SearchDnResolver"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    setLdapDnPrincipal="true"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    authorizationFilter="uid={1}"
+    authorizationFilterArgs="7"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="{trustCertificates=file:src/test/resources/ed.trust.crt}";
+};
+
+vt-ldap-random {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389 ldap://ed-dne.middleware.vt.edu"
+    baseDn="ou=test,dc=vt,dc=edu"
+    connectionHandler="edu.vt.middleware.ldap.handler.TlsConnectionHandler{{connectionStrategy=RANDOM}}"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    setLdapDnPrincipal="true"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="{trustCertificates=file:src/test/resources/ed.trust.crt}";
+};
+
+vt-ldap-filter {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389 ldap://ed-dne.middleware.vt.edu"
+    baseDn="ou=test,dc=vt,dc=edu"
+    connectionHandler="edu.vt.middleware.ldap.handler.TlsConnectionHandler{{connectionStrategy=ACTIVE_PASSIVE}{connectionRetryExceptions=javax.naming.CommunicationException}}"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier{foo=test,bar=false}"
+    handlerIgnoreExceptions="javax.naming.NamingException"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    allowMultipleDns="true"
+    authorizationFilter="uid={1}"
+    authorizationFilterArgs="7"
+    userFilter="(&(mail={0})(objectClass={1}))"
+    userFilterArgs="person"
+    sslSocketFactory="{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-handler {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    handlerIgnoreExceptions="javax.naming.NamingException"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    authorizationHandlers="edu.vt.middleware.ldap.auth.handler.TestAuthorizationHandler"
+    userFilter="(&(mail={0})(objectClass={1}))"
+    userFilterArgs="person"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-roles {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    storePass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+  edu.vt.middleware.ldap.jaas.LdapRoleAuthorizationModule optional
+    useFirstPass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389/ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    roleFilter="(mail={1})"
+    roleAttribute="objectClass"
+    sslSocketFactory="{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-roles-recursive {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    storePass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+  edu.vt.middleware.ldap.jaas.LdapRoleAuthorizationModule required
+    useFirstPass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    roleFilter="(member={0})"
+    roleAttribute="uugid"
+    searchResultHandlers="edu.vt.middleware.ldap.handler.FqdnSearchResultHandler,edu.vt.middleware.ldap.handler.RecursiveSearchResultHandler{{searchAttribute=member}{mergeAttributes=uugid}}"
+    sslSocketFactory="{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-use-first {
+  edu.vt.middleware.ldap.jaas.TestLoginModule required;
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    useFirstPass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    defaultRole="test-role1,test-role2"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-try-first {
+  edu.vt.middleware.ldap.jaas.TestLoginModule required;
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    tryFirstPass="true"
+    storePass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+  edu.vt.middleware.ldap.jaas.LdapRoleAuthorizationModule optional
+    useFirstPass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389/ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    roleFilter="(mail={1})"
+    roleAttribute="objectClass"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-sufficient {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule sufficient
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    authorizationFilter="departmentNumber=0000"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+  edu.vt.middleware.ldap.jaas.LdapLoginModule sufficient
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    baseDn="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    authorizationFilter="departmentNumber=0827"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-roles-only {
+  edu.vt.middleware.ldap.jaas.LdapRoleAuthorizationModule required
+    useFirstPass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389/ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    roleFilter="(uid=7)"
+    roleAttribute="departmentNumber,objectClass"
+    principalGroupName="Principals"
+    roleGroupName="Roles"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-dn-roles-only {
+  edu.vt.middleware.ldap.jaas.LdapDnAuthorizationModule required
+    storePass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389/ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+  edu.vt.middleware.ldap.jaas.LdapRoleAuthorizationModule required
+    useFirstPass="true"
+    ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389/ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    bindDn="uid=1,ou=test,dc=vt,dc=edu"
+    bindCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    roleFilter="(mail={1})"
+    roleAttribute="departmentNumber,objectClass"
+    principalGroupName="Principals"
+    roleGroupName="Roles"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+vt-ldap-deprecated {
+  edu.vt.middleware.ldap.jaas.LdapLoginModule required
+    host="ldap-test-1.middleware.vt.edu"
+    port="10389"
+    base="ou=test,dc=vt,dc=edu"
+    tls="true"
+    hostnameVerifier="edu.vt.middleware.ldap.AnyHostnameVerifier"
+    serviceUser="uid=1,ou=test,dc=vt,dc=edu"
+    serviceCredential="VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2"
+    userField="mail"
+    userRoleAttribute="departmentNumber"
+    sslSocketFactory="edu.vt.middleware.ldap.ssl.TLSSocketFactory{edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig{{trustStore=classpath:/ed.truststore} {trustStoreType=BKS}}}";
+};
+
+com.sun.security.jgss.initiate {
+  com.sun.security.auth.module.Krb5LoginModule required
+    doNotPrompt="true"
+    debug="true"
+    principal="test3"
+    useKeyTab="true"
+    keyTab="src/test/resources/krb5.keytab";
+};
diff --git a/src/test/resources/log4j.xml b/src/test/resources/log4j.xml
new file mode 100644
index 0000000..c562a12
--- /dev/null
+++ b/src/test/resources/log4j.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
+
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"
+                     debug="false">
+
+  <appender name="CONSOLE"
+            class="org.apache.log4j.ConsoleAppender">
+    <param name="Target" value="System.out"/>
+    <layout class="org.apache.log4j.PatternLayout">
+      <param name="ConversionPattern" value="%X{host} %d %-5p [%c] %m%n"/>
+    </layout>
+  </appender>
+
+  <category name="edu.vt.middleware.ldap"
+            additivity="false">
+    <priority value="INFO"/>
+    <appender-ref ref="CONSOLE"/>
+  </category>
+
+  <category name="org.springframework"
+            additivity="false">
+    <priority value="ERROR"/>
+    <appender-ref ref="CONSOLE"/>
+  </category>
+
+  <root>
+    <priority value="INFO"/>
+    <appender-ref ref="CONSOLE"/>
+  </root>
+
+</log4j:configuration>
diff --git a/src/test/resources/spring-context.xml b/src/test/resources/spring-context.xml
new file mode 100644
index 0000000..45a8746
--- /dev/null
+++ b/src/test/resources/spring-context.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xmlns:p="http://www.springframework.org/schema/p"
+  xmlns:util="http://www.springframework.org/schema/util"
+  xsi:schemaLocation="
+http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+  <bean id="ldap"
+    class="edu.vt.middleware.ldap.Ldap"
+    p:ldapConfig-ref="ldapConfig"
+  />
+  
+  <bean id="ldapConfig"
+    class="edu.vt.middleware.ldap.LdapConfig"
+    p:ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    p:tls="true"
+    p:authtype="EXTERNAL"
+    p:searchScope="SUBTREE">
+    <property name="sslSocketFactory">
+      <bean class="edu.vt.middleware.ldap.ssl.TLSSocketFactory"
+        init-method="initialize">
+        <property name="SSLContextInitializer">
+          <bean
+            factory-bean="sslContextInitializerFactory"
+            factory-method="createSSLContextInitializer" />
+        </property>
+      </bean>
+    </property>
+  </bean>
+  
+  <bean id="sslContextInitializerFactory"
+    class="edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig"
+    p:keyStore="classpath:/ed.keystore"
+    p:keyStoreType="BKS"
+    p:keyStorePassword="changeit"
+    p:trustStore="classpath:/ed.truststore"
+    p:trustStoreType="BKS"
+    p:trustStorePassword="changeit"
+  />
+  
+</beans>
diff --git a/src/test/resources/spring-pool-context.xml b/src/test/resources/spring-pool-context.xml
new file mode 100644
index 0000000..d3e223e
--- /dev/null
+++ b/src/test/resources/spring-pool-context.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xmlns:p="http://www.springframework.org/schema/p"
+  xmlns:util="http://www.springframework.org/schema/util"
+  xsi:schemaLocation="
+http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
+http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd">
+
+  <bean id="ldapFactory"
+    class="edu.vt.middleware.ldap.pool.DefaultLdapFactory">
+    <constructor-arg index="0" ref="ldapConfig"/>
+  </bean>
+  
+  <bean id="ldapPool"
+    class="edu.vt.middleware.ldap.pool.BlockingLdapPool"
+    init-method="initialize"
+    p:blockWaitTime="5000">
+    <constructor-arg index="0">
+      <bean class="edu.vt.middleware.ldap.pool.LdapPoolConfig"
+        p:minPoolSize="5"
+        p:maxPoolSize="20"
+        p:validatePeriodically="true"
+        p:validateTimerPeriod="30000"
+        p:expirationTime="600000"
+        p:pruneTimerPeriod="60000"
+      />
+    </constructor-arg>
+    <constructor-arg index="1" ref="ldapFactory"/>
+  </bean>
+  
+  <bean id="ldapConfig"
+    class="edu.vt.middleware.ldap.LdapConfig"
+    p:ldapUrl="ldap://ldap-test-1.middleware.vt.edu:10389"
+    p:tls="true"
+    p:authtype="EXTERNAL"
+    p:searchScope="SUBTREE">
+    <property name="sslSocketFactory">
+      <bean class="edu.vt.middleware.ldap.ssl.TLSSocketFactory"
+        init-method="initialize">
+        <property name="SSLContextInitializer">
+          <bean
+            factory-bean="sslContextInitializerFactory"
+            factory-method="createSSLContextInitializer" />
+        </property>
+      </bean>
+    </property>
+  </bean>
+
+  <bean id="sslContextInitializerFactory"
+    class="edu.vt.middleware.ldap.ssl.KeyStoreCredentialConfig"
+    p:keyStore="classpath:/ed.keystore"
+    p:keyStoreType="BKS"
+    p:keyStorePassword="changeit"
+    p:trustStore="classpath:/ed.truststore"
+    p:trustStoreType="BKS"
+    p:trustStorePassword="changeit"
+  />
+
+</beans>
diff --git a/src/test/resources/vt-ldap.truststore b/src/test/resources/vt-ldap.truststore
new file mode 100644
index 0000000..98ed077
Binary files /dev/null and b/src/test/resources/vt-ldap.truststore differ
diff --git a/src/test/resources/web.xml b/src/test/resources/web.xml
new file mode 100644
index 0000000..271e30f
--- /dev/null
+++ b/src/test/resources/web.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE web-app
+    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+    "http://java.sun.com/dtd/web-app_2_3.dtd">
+
+<web-app>
+  <servlet>
+    <servlet-name>LdifSearch</servlet-name>
+    <servlet-class>edu.vt.middleware.ldap.servlets.SearchServlet</servlet-class>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.propertiesFile</param-name>
+      <param-value>/ldap.properties</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.poolPropertiesFile</param-name>
+      <param-value>/ldap.pool.properties</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.poolType</param-name>
+      <param-value>BLOCKING</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.outputFormat</param-name>
+      <param-value>LDIF</param-value>
+    </init-param>
+  </servlet>
+  <servlet>
+    <servlet-name>DsmlSearch</servlet-name>
+    <servlet-class>edu.vt.middleware.ldap.servlets.SearchServlet</servlet-class>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.propertiesFile</param-name>
+      <param-value>/ldap.properties</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.poolPropertiesFile</param-name>
+      <param-value>/ldap.pool.properties</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.poolType</param-name>
+      <param-value>SOFTLIMIT</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.outputFormat</param-name>
+      <param-value>DSML</param-value>
+    </init-param>
+  </servlet>
+  <servlet>
+    <servlet-name>AttributeSearch</servlet-name>
+    <servlet-class>edu.vt.middleware.ldap.servlets.AttributeServlet</servlet-class>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.propertiesFile</param-name>
+      <param-value>/ldap.properties</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.poolPropertiesFile</param-name>
+      <param-value>/ldap.pool.properties</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.poolType</param-name>
+      <param-value>SHARED</param-value>
+    </init-param>
+  </servlet>
+  <servlet>
+    <servlet-name>Login</servlet-name>
+    <servlet-class>edu.vt.middleware.ldap.servlets.LoginServlet</servlet-class>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.propertiesFile</param-name>
+      <param-value>/ldap.tls.properties</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.sessionId</param-name>
+      <param-value>vt-ldap.User</param-value>
+    </init-param>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.loginUrl</param-name>
+      <param-value>SessionCheck</param-value>
+    </init-param>
+  </servlet>
+  <servlet>
+    <servlet-name>Logout</servlet-name>
+    <servlet-class>edu.vt.middleware.ldap.servlets.LogoutServlet</servlet-class>
+    <init-param>
+      <param-name>edu.vt.middleware.ldap.servlets.sessionId</param-name>
+      <param-value>vt-ldap.User</param-value>
+    </init-param>
+  </servlet>
+  <servlet>
+    <servlet-name>SessionCheck</servlet-name>
+    <servlet-class>edu.vt.middleware.ldap.servlets.SessionCheck</servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>LdifSearch</servlet-name>
+    <url-pattern>/LdifSearch</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>DsmlSearch</servlet-name>
+    <url-pattern>/DsmlSearch</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>AttributeSearch</servlet-name>
+    <url-pattern>/AttributeSearch</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>Login</servlet-name>
+    <url-pattern>/Login</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>Logout</servlet-name>
+    <url-pattern>/Logout</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>SessionCheck</servlet-name>
+    <url-pattern>/SessionCheck</url-pattern>
+  </servlet-mapping>
+</web-app>
diff --git a/src/test/testng/testng.xml b/src/test/testng/testng.xml
new file mode 100644
index 0000000..48107d4
--- /dev/null
+++ b/src/test/testng/testng.xml
@@ -0,0 +1,355 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
+
+<suite name="vt-ldap" verbose="1" parallel="tests" thread-count="1">
+
+  <!-- ldap test parameters -->
+  <parameter name="createEntry2"
+             value="/edu/vt/middleware/ldap/createLdapEntry-2.ldif"/>
+  <parameter name="createEntry3"
+             value="/edu/vt/middleware/ldap/createLdapEntry-3.ldif"/>
+  <parameter name="createEntry4"
+             value="/edu/vt/middleware/ldap/createLdapEntry-4.ldif"/>
+  <parameter name="createEntry5"
+             value="/edu/vt/middleware/ldap/createLdapEntry-5.ldif"/>
+  <parameter name="createEntry6"
+             value="/edu/vt/middleware/ldap/createLdapEntry-6.ldif"/>
+  <parameter name="createEntry7"
+             value="/edu/vt/middleware/ldap/createLdapEntry-7.ldif"/>
+  <parameter name="createEntry8"
+             value="/edu/vt/middleware/ldap/createLdapEntry-8.ldif"/>
+  <parameter name="createEntry9"
+             value="/edu/vt/middleware/ldap/createLdapEntry-9.ldif"/>
+  <parameter name="createEntry10"
+             value="/edu/vt/middleware/ldap/createLdapEntry-10.ldif"/>
+  <parameter name="createEntry11"
+             value="/edu/vt/middleware/ldap/createLdapEntry-11.ldif"/>
+  <parameter name="createEntry12"
+             value="/edu/vt/middleware/ldap/createLdapEntry-12.ldif"/>
+
+  <parameter name="createGroup2"
+             value="/edu/vt/middleware/ldap/createGroupEntry-2.ldif"/>
+  <parameter name="createGroup3"
+             value="/edu/vt/middleware/ldap/createGroupEntry-3.ldif"/>
+  <parameter name="createGroup4"
+             value="/edu/vt/middleware/ldap/createGroupEntry-4.ldif"/>
+  <parameter name="createGroup5"
+             value="/edu/vt/middleware/ldap/createGroupEntry-5.ldif"/>
+  <parameter name="createGroup6"
+             value="/edu/vt/middleware/ldap/createGroupEntry-6.ldif"/>
+  <parameter name="createGroup7"
+             value="/edu/vt/middleware/ldap/createGroupEntry-7.ldif"/>
+  <parameter name="createGroup8"
+             value="/edu/vt/middleware/ldap/createGroupEntry-8.ldif"/>
+  <parameter name="createGroup9"
+             value="/edu/vt/middleware/ldap/createGroupEntry-9.ldif"/>
+
+  <parameter name="createSpecialCharsEntry"
+             value="/edu/vt/middleware/ldap/specialChars.ldif"/>
+  <parameter name="createSpecialCharsEntry2"
+             value="/edu/vt/middleware/ldap/specialChars-2.ldif"/>
+
+  <parameter name="multipleLdifResultsIn"
+             value="/edu/vt/middleware/ldap/multipleEntriesIn.ldif"/>
+  <parameter name="multipleLdifResultsOut"
+             value="/edu/vt/middleware/ldap/multipleEntriesOut.ldif"/>
+  <parameter name="ldifEntry"
+             value="/edu/vt/middleware/ldap/dfisher.ldif"/>
+  <parameter name="ldifSortedEntry"
+             value="/edu/vt/middleware/ldap/dfisher.sorted.ldif"/>
+
+  <parameter name="dsmlv1Entry"
+             value="/edu/vt/middleware/ldap/dfisher.dsmlv1"/>
+  <parameter name="dsmlv1SortedEntry"
+             value="/edu/vt/middleware/ldap/dfisher.sorted.dsmlv1"/>
+  <parameter name="dsmlv2Entry"
+             value="/edu/vt/middleware/ldap/dfisher.dsmlv2"/>
+  <parameter name="dsmlv2SortedEntry"
+             value="/edu/vt/middleware/ldap/dfisher.sorted.dsmlv2"/>
+
+  <parameter name="searchResults2"
+             value="/edu/vt/middleware/ldap/searchResults-2.ldif"/>
+  <parameter name="searchResults3"
+             value="/edu/vt/middleware/ldap/searchResults-3.ldif"/>
+  <parameter name="searchResults4"
+             value="/edu/vt/middleware/ldap/searchResults-4.ldif"/>
+  <parameter name="searchResults5"
+             value="/edu/vt/middleware/ldap/searchResults-5.ldif"/>
+  <parameter name="searchResults6"
+             value="/edu/vt/middleware/ldap/searchResults-6.ldif"/>
+  <parameter name="searchResults7"
+             value="/edu/vt/middleware/ldap/searchResults-7.ldif"/>
+  <parameter name="searchResults8"
+             value="/edu/vt/middleware/ldap/searchResults-8.ldif"/>
+  <parameter name="searchResults9"
+             value="/edu/vt/middleware/ldap/searchResults-9.ldif"/>
+  <parameter name="searchResults10"
+             value="/edu/vt/middleware/ldap/searchResults-10.ldif"/>
+  <parameter name="searchResults11"
+             value="/edu/vt/middleware/ldap/searchResults-11.ldif"/>
+  <parameter name="searchResults12"
+             value="/edu/vt/middleware/ldap/searchResults-12.ldif"/>
+
+  <parameter name="renameOldDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="renameNewDn" value="uid=1002,ou=test,dc=vt,dc=edu"/>
+
+  <parameter name="compareDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="compareFilter" value="(departmentNumber={0})"/>
+  <parameter name="compareFilterArgs" value="0822"/>
+
+  <parameter name="searchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="searchFilter" value="(uid={0})"/>
+  <parameter name="searchFilterArgs" value="2"/>
+  <parameter name="searchReturnAttrs" value="departmentNumber|givenName|sn"/>
+  <parameter name="searchResults"
+             value="/edu/vt/middleware/ldap/searchResults-2.ldif"/>
+
+  <parameter name="searchAttributesDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="searchAttributesFilter" value="mail=jdoe2 at vt.edu"/>
+  <parameter name="searchAttributesReturnAttrs" value="departmentNumber|givenName|sn"/>
+  <parameter name="searchAttributesResults"
+             value="/edu/vt/middleware/ldap/searchAttributesResults-2.ldif"/>
+
+  <parameter name="pagedSearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="pagedSearchFilter" value="uugid=*"/>
+  <parameter name="pagedSearchResults"
+             value="/edu/vt/middleware/ldap/pagedResults.ldif"/>
+
+  <parameter name="recursiveSearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="recursiveSearchFilter" value="(uugid={0})"/>
+  <parameter name="recursiveSearchFilterArgs" value="group2"/>
+  <parameter name="recursiveAttributeHandlerResults"
+             value="/edu/vt/middleware/ldap/recursiveAttributeHandlerResults.ldif"/>
+  <parameter name="recursiveSearchResultHandlerResults"
+             value="/edu/vt/middleware/ldap/recursiveSearchResultHandlerResults.ldif"/>
+
+  <parameter name="mergeSearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="mergeSearchFilter" value="(|(uugid=group3)(uugid=group4)(uugid=group5))"/>
+  <parameter name="mergeSearchResults"
+             value="/edu/vt/middleware/ldap/mergeResults.ldif"/>
+
+  <parameter name="mergeDuplicateSearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="mergeDuplicateSearchFilter" value="(|(uugid=group3)(uugid=group4)(uugid=group5))"/>
+  <parameter name="mergeDuplicateSearchResults"
+             value="/edu/vt/middleware/ldap/mergeDuplicateResults.ldif"/>
+
+  <parameter name="binarySearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="binarySearchFilter" value="uid=2"/>
+  <parameter name="binarySearchReturnAttr" value="jpegPhoto"/>
+  <parameter name="binarySearchResult"
+             value="/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnGH0R0ryvhlAAAdCBPR [...]
+
+  <parameter name="searchExceptionDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="searchExceptionFilter" value="(|(uugid=group2)(uugid=group3)(uugid=group4))"/>
+  <parameter name="searchExceptionResultsSize" value="3"/>
+
+  <parameter name="specialCharSearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="specialCharSearchFilter" value="(uid=17893)"/>
+  <parameter name="specialCharSearchResults"
+             value="/edu/vt/middleware/ldap/specialChars.ldif"/>
+
+  <parameter name="rewriteSearchDn" value="dc=blah"/>
+  <parameter name="rewriteSearchFilter" value="(uid=17893)"/>
+  <parameter name="rewriteSearchResults"
+             value="/edu/vt/middleware/ldap/specialChars.ldif"/>
+
+  <parameter name="listDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="listResults" value="uid=1|uid=2"/>
+
+  <parameter name="listBindingsDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="listBindingsResults" value="uid=1|uid=2"/>
+
+  <parameter name="getAttributesDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="getAttributesReturnAttrs" value="departmentNumber|givenName|sn"/>
+  <parameter name="getAttributesResults"
+             value="departmentNumber=0822|givenName=John|sn=Doe"/>
+
+  <parameter name="getAttributesBase64Dn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="getAttributesBase64ReturnAttrs" value="sn|jpegPhoto"/>
+  <parameter name="getAttributesBase64Results"
+             value="sn=Doe|jpegPhoto=/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0bLMRcnZvsdpnG [...]
+
+  <parameter name="getSchemaDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="getSchemaResults"
+             value="/edu/vt/middleware/ldap/getSchemaResults.ldif"/>
+
+  <parameter name="addAttributeDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="addAttributeAttribute"
+             value="title=Test User|title=Best User"/>
+
+  <parameter name="addAttributesDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="addAttributesAttributes"
+             value="telephoneNumber=15408675309|homePhone=15555555555"/>
+
+  <parameter name="replaceAttributeDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="replaceAttributeAttribute"
+             value="title=Unit Test User|title=Best Test User"/>
+
+  <parameter name="replaceAttributesDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="replaceAttributesAttributes"
+             value="telephoneNumber=12223334444|homePhone=155566677777"/>
+
+  <parameter name="removeAttributeDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="removeAttributeAttribute"
+             value="title=Unit Test User|title=Best Test User"/>
+
+  <parameter name="removeAttributesDn" value="uid=2,ou=test,dc=vt,dc=edu"/>
+  <parameter name="removeAttributesAttributes"
+             value="telephoneNumber=12223334444|homePhone=155566677777"/>
+
+  <parameter name="krb5Realm" value="VT.EDU"/>
+  <parameter name="krb5Kdc" value="ldap-test-1.middleware.vt.edu"/>
+  <parameter name="gssApiSearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="gssApiSearchFilter" value="(uid={0})"/>
+  <parameter name="gssApiSearchFilterArgs" value="2"/>
+  <parameter name="gssApiSearchReturnAttrs" value="departmentNumber|givenName|sn"/>
+  <parameter name="gssApiSearchResults"
+             value="/edu/vt/middleware/ldap/searchResults-2.ldif"/>
+
+  <parameter name="loadPropertiesUrl" value="ldap://ldap-test-1.middleware.vt.edu:389 ldap://ldap-test-1.middleware.vt.edu:10389"/>
+  <parameter name="loadPropertiesBaseDn" value="ou=test,dc=vt,dc=edu"/>
+
+  <parameter name="getDnUid" value="3"/>
+  <parameter name="getDnUser" value="jdoe3 at vt.edu"/>
+  <parameter name="getDnDuplicateFilter" value="uid=*"/>
+
+  <parameter name="authenticateDn" value="uid=3,ou=test,dc=vt,dc=edu"/>
+  <parameter name="authenticateDnCredential" value="password3"/>
+  <parameter name="authenticateDnFilter" value="departmentNumber={1}"/>
+  <parameter name="authenticateDnFilterArgs" value="0823"/>
+  <parameter name="authenticateDnReturnAttrs" value="givenName|sn"/>
+  <parameter name="authenticateDnResults" value="givenName=Joho|sn=Dof"/>
+
+  <parameter name="authenticateUser" value="jdoe3 at vt.edu"/>
+  <parameter name="authenticateCredential" value="password3"/>
+  <parameter name="authenticateFilter" value="departmentNumber=0823"/>
+  <parameter name="authenticateReturnAttrs" value="givenName|sn"/>
+  <parameter name="authenticateResults" value="givenName=Joho|sn=Dof"/>
+
+  <parameter name="authenticateSpecialCharsUser" value="17894"/>
+  <parameter name="authenticateSpecialCharsCredential" value="password2"/>
+
+  <parameter name="digestMd5User" value="test3"/>
+  <parameter name="digestMd5Credential" value="password"/>
+
+  <parameter name="cramMd5User" value="test3"/>
+  <parameter name="cramMd5Credential" value="password"/>
+
+  <parameter name="toSearchResultsDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="toSearchResultsFilter" value="uid=4"/>
+  <parameter name="toSearchResultsAttrs" value="departmentNumber|givenName|sn"/>
+  <parameter name="toSearchResultsResults"
+             value="/edu/vt/middleware/ldap/searchResults-4.ldif"/>
+
+  <parameter name="cliSearchArgs"
+             value="-ldapUrl|ldap://ldap-test-1.middleware.vt.edu:10389|-baseDn|ou=test,dc=vt,dc=edu|-bindDn|uid=1,ou=test,dc=vt,dc=edu|-bindCredential|VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2|-query|(mail=jdoe5@vt.edu)|departmentNumber|givenName|sn"/>
+  <parameter name="cliSearchResults"
+             value="/edu/vt/middleware/ldap/searchResults-5.ldif"/>
+
+  <parameter name="cliAuthArgs"
+             value="-ldapUrl|ldap://ldap-test-1.middleware.vt.edu:10389|-baseDn|ou=test,dc=vt,dc=edu|-tls|true|-hostnameVerifier|edu.vt.middleware.ldap.AnyHostnameVerifier|-bindDn|uid=1,ou=test,dc=vt,dc=edu|-bindCredential|VKSxXwlU7YssGl1foLMH2mGMWkifbODb1djfJ4t2|-userField|mail|-user|jdoe6@vt.edu|-credential|password6|-authorizationFilter|(departmentNumber=0826)|departmentNumber|givenName|sn"/>
+  <parameter name="cliAuthResults"
+             value="/edu/vt/middleware/ldap/searchResults-6.ldif"/>
+
+  <parameter name="jaasDn" value="uid=7,ou=test,dc=vt,dc=edu"/>
+  <parameter name="jaasUser" value="jdoe7 at vt.edu"/>
+  <parameter name="jaasUserRole" value="0827"/>
+  <parameter name="jaasUserRoleDefault" value="0827|test-role1|test-role2"/>
+  <parameter name="jaasRole" value="inetOrgPerson|organizationalPerson|person|top|virginiaTechPerson"/>
+  <parameter name="jaasRoleCombined" value="0827|inetOrgPerson|organizationalPerson|person|top|virginiaTechPerson"/>
+  <parameter name="jaasRoleCombinedRecursive" value="0827|group6|group7|group8|group9"/>
+  <parameter name="jaasCredential" value="password7"/>
+
+  <parameter name="webXml" value="src/test/resources/web.xml"/>
+
+  <parameter name="ldifSearchServletQuery" value="mail=jdoe8 at vt.edu"/>
+  <parameter name="ldifSearchServletAttrs" value="departmentNumber|givenName|sn|jpegPhoto"/>
+  <parameter name="ldifSearchServletLdif" value="/edu/vt/middleware/ldap/searchResults-8.ldif"/>
+
+  <parameter name="dsmlSearchServletQuery" value="uid=8"/>
+  <parameter name="dsmlSearchServletAttrs" value="departmentNumber|givenName|sn|jpegPhoto"/>
+  <parameter name="dsmlSearchServletLdif" value="/edu/vt/middleware/ldap/searchResults-8.ldif"/>
+
+  <parameter name="attributeServletQuery" value="(&(givenName=johu)(sn=dol))"/>
+  <parameter name="attributeServletAttr" value="jpegPhoto"/>
+  <parameter name="attributeServletValue" value="/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAFA3PEY8MlBGQUZaVVBfeMiCeG5uePWvuZHI////////////////////////////////////////////////////2wBDAVVaWnhpeOuCguv/////////////////////////////////////////////////////////////////////////wAARCAANABcDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAIEAQP/xAAiEAEAAgIABQUAAAAAAAAAAAABAAMCEQQSMUFxISIjMnP/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A71fHZZW+ge48MWqvG0b [...]
+
+  <parameter name="sessionManagerUser" value="jdoe10 at vt.edu"/>
+  <parameter name="sessionManagerPassword" value="password10"/>
+
+  <parameter name="dsmlSearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="dsmlSearchFilter" value="(uid=11)"/>
+
+  <parameter name="ldifSearchDn" value="ou=test,dc=vt,dc=edu"/>
+  <parameter name="ldifSearchFilter" value="(uid=12)"/>
+
+  <parameter name="ldapHost" value="ed-dev"/>
+  <parameter name="sleepTime" value="10000"/>
+
+  <test name="coretests" parallel="methods" thread-count="12">
+    <groups>
+      <run>
+        <include name="ldaptest" />
+        <include name="authtest" />
+        <include name="beantest" />
+        <include name="jaastest" />
+        <include name="servlettest" />
+        <include name="dsmltest" />
+        <include name="ldiftest" />
+        <include name="ssltest" />
+        <include name="wikitest" />
+      </run>
+    </groups>
+    <packages>
+      <package name="edu.vt.middleware.ldap.*" />
+    </packages>
+  </test>
+  <test name="clitests">
+    <groups>
+      <run>
+        <include name="ldapclitest" />
+        <include name="authclitest" />
+      </run>
+    </groups>
+    <packages>
+      <package name="edu.vt.middleware.ldap.*" />
+    </packages>
+  </test>
+  <test name="conntest">
+    <groups>
+      <run>
+        <include name="conntest" />
+      </run>
+    </groups>
+    <packages>
+      <package name="edu.vt.middleware.ldap.*" />
+    </packages>
+  </test>
+<!-- must run separate from other tests
+  <test name="loadtests" parallel="methods" thread-count="12">
+    <groups>
+      <run>
+        <include name="authloadtest" />
+      </run>
+    </groups>
+    <packages>
+      <package name="edu.vt.middleware.ldap.*" />
+    </packages>
+  </test>
+  <test name="pooltests" parallel="methods" thread-count="12">
+    <groups>
+      <run>
+        <include name="softlimitpooltest" />
+        <include name="blockingpooltest" />
+        <include name="blockingtimeoutpooltest" />
+        <include name="sharedpooltest" />
+        <include name="connstrategypooltest" />
+        <include name="comparisonpooltest" />
+      </run>
+    </groups>
+    <packages>
+      <package name="edu.vt.middleware.ldap.pool.*" />
+    </packages>
+  </test>
+-->
+</suite>

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/libvt-ldap-java.git



More information about the pkg-java-commits mailing list